@codefast/di 0.5.0-canary.6 → 0.5.0-canary.8

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 (195) hide show
  1. package/CHANGELOG.md +123 -0
  2. package/README.md +15 -4
  3. package/dist/binding.d.ts +78 -19
  4. package/dist/binding.d.ts.map +1 -1
  5. package/dist/binding.js +46 -2
  6. package/dist/binding.js.map +1 -1
  7. package/dist/constructor-type.d.ts +4 -5
  8. package/dist/constructor-type.d.ts.map +1 -1
  9. package/dist/container/binding-builders.d.ts +56 -0
  10. package/dist/container/binding-builders.d.ts.map +1 -0
  11. package/dist/container/binding-builders.js +196 -0
  12. package/dist/container/binding-builders.js.map +1 -0
  13. package/dist/{container.d.ts → container/container.d.ts} +2 -2
  14. package/dist/container/container.d.ts.map +1 -0
  15. package/dist/container/container.js +624 -0
  16. package/dist/container/container.js.map +1 -0
  17. package/dist/decorators/inject.d.ts +10 -1
  18. package/dist/decorators/inject.d.ts.map +1 -1
  19. package/dist/decorators/inject.js +2 -2
  20. package/dist/decorators/inject.js.map +1 -1
  21. package/dist/decorators/injectable.js +3 -3
  22. package/dist/decorators/injectable.js.map +1 -1
  23. package/dist/errors.d.ts +14 -0
  24. package/dist/errors.d.ts.map +1 -1
  25. package/dist/errors.js +17 -0
  26. package/dist/errors.js.map +1 -1
  27. package/dist/index.d.ts +13 -8
  28. package/dist/index.d.ts.map +1 -1
  29. package/dist/index.js +9 -5
  30. package/dist/index.js.map +1 -1
  31. package/dist/introspection/dependency-graph.d.ts.map +1 -0
  32. package/dist/{dependency-graph.js → introspection/dependency-graph.js} +1 -1
  33. package/dist/introspection/dependency-graph.js.map +1 -0
  34. package/dist/{graph-adapters → introspection/graph-adapters}/cytoscape.d.ts +1 -1
  35. package/dist/introspection/graph-adapters/cytoscape.d.ts.map +1 -0
  36. package/dist/introspection/graph-adapters/cytoscape.js.map +1 -0
  37. package/dist/introspection/graph-adapters/dot.d.ts +6 -0
  38. package/dist/introspection/graph-adapters/dot.d.ts.map +1 -0
  39. package/dist/introspection/graph-adapters/dot.js.map +1 -0
  40. package/dist/{graph-adapters → introspection/graph-adapters}/reactflow.d.ts +2 -2
  41. package/dist/introspection/graph-adapters/reactflow.d.ts.map +1 -0
  42. package/dist/{graph-adapters → introspection/graph-adapters}/reactflow.js +11 -1
  43. package/dist/introspection/graph-adapters/reactflow.js.map +1 -0
  44. package/dist/{inspector.d.ts → introspection/inspector.d.ts} +3 -8
  45. package/dist/introspection/inspector.d.ts.map +1 -0
  46. package/dist/{inspector.js → introspection/inspector.js} +23 -23
  47. package/dist/introspection/inspector.js.map +1 -0
  48. package/dist/metadata/metadata-keys.d.ts +3 -6
  49. package/dist/metadata/metadata-keys.d.ts.map +1 -1
  50. package/dist/metadata/metadata-keys.js +3 -6
  51. package/dist/metadata/metadata-keys.js.map +1 -1
  52. package/dist/metadata/symbol-metadata-reader.d.ts +1 -1
  53. package/dist/metadata/symbol-metadata-reader.d.ts.map +1 -1
  54. package/dist/metadata/symbol-metadata-reader.js +4 -4
  55. package/dist/metadata/symbol-metadata-reader.js.map +1 -1
  56. package/dist/module.d.ts +9 -2
  57. package/dist/module.d.ts.map +1 -1
  58. package/dist/module.js +9 -2
  59. package/dist/module.js.map +1 -1
  60. package/dist/registry.d.ts +17 -13
  61. package/dist/registry.d.ts.map +1 -1
  62. package/dist/registry.js +94 -63
  63. package/dist/registry.js.map +1 -1
  64. package/dist/resolution/activation-need.d.ts +25 -0
  65. package/dist/resolution/activation-need.d.ts.map +1 -0
  66. package/dist/resolution/activation-need.js +64 -0
  67. package/dist/resolution/activation-need.js.map +1 -0
  68. package/dist/resolution/binding-lookup-cache.d.ts +41 -0
  69. package/dist/resolution/binding-lookup-cache.d.ts.map +1 -0
  70. package/dist/resolution/binding-lookup-cache.js +102 -0
  71. package/dist/resolution/binding-lookup-cache.js.map +1 -0
  72. package/dist/resolution/binding-scope.d.ts.map +1 -0
  73. package/dist/resolution/binding-scope.js.map +1 -0
  74. package/dist/resolution/binding-select.d.ts.map +1 -0
  75. package/dist/{binding-select.js → resolution/binding-select.js} +17 -7
  76. package/dist/resolution/binding-select.js.map +1 -0
  77. package/dist/resolution/class-introspector.d.ts +27 -0
  78. package/dist/resolution/class-introspector.d.ts.map +1 -0
  79. package/dist/resolution/class-introspector.js +60 -0
  80. package/dist/resolution/class-introspector.js.map +1 -0
  81. package/dist/resolution/constraints.d.ts.map +1 -0
  82. package/dist/resolution/constraints.js.map +1 -0
  83. package/dist/resolution/diagnostics.d.ts +41 -0
  84. package/dist/resolution/diagnostics.d.ts.map +1 -0
  85. package/dist/resolution/diagnostics.js +18 -0
  86. package/dist/resolution/diagnostics.js.map +1 -0
  87. package/dist/{environment.d.ts → resolution/environment.d.ts} +24 -7
  88. package/dist/resolution/environment.d.ts.map +1 -0
  89. package/dist/resolution/environment.js +126 -0
  90. package/dist/resolution/environment.js.map +1 -0
  91. package/dist/resolution/instantiation-plan.d.ts +67 -0
  92. package/dist/resolution/instantiation-plan.d.ts.map +1 -0
  93. package/dist/resolution/instantiation-plan.js +183 -0
  94. package/dist/resolution/instantiation-plan.js.map +1 -0
  95. package/dist/{lifecycle.d.ts → resolution/lifecycle.d.ts} +5 -3
  96. package/dist/resolution/lifecycle.d.ts.map +1 -0
  97. package/dist/{lifecycle.js → resolution/lifecycle.js} +23 -14
  98. package/dist/resolution/lifecycle.js.map +1 -0
  99. package/dist/resolution/resolution-path.d.ts +22 -0
  100. package/dist/resolution/resolution-path.d.ts.map +1 -0
  101. package/dist/resolution/resolution-path.js +40 -0
  102. package/dist/resolution/resolution-path.js.map +1 -0
  103. package/dist/resolution/resolve-options.d.ts.map +1 -0
  104. package/dist/resolution/resolve-options.js.map +1 -0
  105. package/dist/resolution/resolver.d.ts +40 -0
  106. package/dist/resolution/resolver.d.ts.map +1 -0
  107. package/dist/resolution/resolver.js +1108 -0
  108. package/dist/resolution/resolver.js.map +1 -0
  109. package/dist/{scope.d.ts → resolution/scope.d.ts} +8 -9
  110. package/dist/resolution/scope.d.ts.map +1 -0
  111. package/dist/resolution/scope.js +80 -0
  112. package/dist/resolution/scope.js.map +1 -0
  113. package/dist/token.d.ts.map +1 -1
  114. package/dist/token.js +0 -3
  115. package/dist/token.js.map +1 -1
  116. package/package.json +19 -94
  117. package/src/binding.ts +137 -22
  118. package/src/constructor-type.ts +4 -5
  119. package/src/container/binding-builders.ts +294 -0
  120. package/src/container/container.ts +825 -0
  121. package/src/decorators/inject.ts +15 -4
  122. package/src/decorators/injectable.ts +3 -3
  123. package/src/errors.ts +21 -0
  124. package/src/index.ts +15 -7
  125. package/src/{dependency-graph.ts → introspection/dependency-graph.ts} +1 -1
  126. package/src/{graph-adapters → introspection/graph-adapters}/cytoscape.ts +1 -1
  127. package/src/{graph-adapters → introspection/graph-adapters}/dot.ts +1 -1
  128. package/src/{graph-adapters → introspection/graph-adapters}/reactflow.ts +13 -2
  129. package/src/{inspector.ts → introspection/inspector.ts} +26 -21
  130. package/src/metadata/metadata-keys.ts +3 -6
  131. package/src/metadata/symbol-metadata-reader.ts +4 -4
  132. package/src/module.ts +14 -6
  133. package/src/registry.ts +97 -64
  134. package/src/resolution/activation-need.ts +81 -0
  135. package/src/resolution/binding-lookup-cache.ts +132 -0
  136. package/src/{binding-select.ts → resolution/binding-select.ts} +17 -7
  137. package/src/resolution/class-introspector.ts +74 -0
  138. package/src/resolution/diagnostics.ts +43 -0
  139. package/src/{environment.ts → resolution/environment.ts} +65 -35
  140. package/src/resolution/instantiation-plan.ts +292 -0
  141. package/src/{lifecycle.ts → resolution/lifecycle.ts} +26 -14
  142. package/src/resolution/resolution-path.ts +62 -0
  143. package/src/resolution/resolver.ts +1526 -0
  144. package/src/resolution/scope.ts +95 -0
  145. package/src/token.ts +0 -3
  146. package/dist/binding-scope.d.ts.map +0 -1
  147. package/dist/binding-scope.js.map +0 -1
  148. package/dist/binding-select.d.ts.map +0 -1
  149. package/dist/binding-select.js.map +0 -1
  150. package/dist/constraints.d.ts.map +0 -1
  151. package/dist/constraints.js.map +0 -1
  152. package/dist/container.d.ts.map +0 -1
  153. package/dist/container.js +0 -853
  154. package/dist/container.js.map +0 -1
  155. package/dist/dependency-graph.d.ts.map +0 -1
  156. package/dist/dependency-graph.js.map +0 -1
  157. package/dist/environment.d.ts.map +0 -1
  158. package/dist/environment.js +0 -100
  159. package/dist/environment.js.map +0 -1
  160. package/dist/graph-adapters/cytoscape.d.ts.map +0 -1
  161. package/dist/graph-adapters/cytoscape.js.map +0 -1
  162. package/dist/graph-adapters/dot.d.ts +0 -6
  163. package/dist/graph-adapters/dot.d.ts.map +0 -1
  164. package/dist/graph-adapters/dot.js.map +0 -1
  165. package/dist/graph-adapters/reactflow.d.ts.map +0 -1
  166. package/dist/graph-adapters/reactflow.js.map +0 -1
  167. package/dist/inspector.d.ts.map +0 -1
  168. package/dist/inspector.js.map +0 -1
  169. package/dist/lifecycle.d.ts.map +0 -1
  170. package/dist/lifecycle.js.map +0 -1
  171. package/dist/resolve-options.d.ts.map +0 -1
  172. package/dist/resolve-options.js.map +0 -1
  173. package/dist/resolver.d.ts +0 -90
  174. package/dist/resolver.d.ts.map +0 -1
  175. package/dist/resolver.js +0 -1208
  176. package/dist/resolver.js.map +0 -1
  177. package/dist/scope.d.ts.map +0 -1
  178. package/dist/scope.js +0 -61
  179. package/dist/scope.js.map +0 -1
  180. package/src/container.ts +0 -1141
  181. package/src/resolver.ts +0 -1626
  182. package/src/scope.ts +0 -77
  183. /package/dist/{dependency-graph.d.ts → introspection/dependency-graph.d.ts} +0 -0
  184. /package/dist/{graph-adapters → introspection/graph-adapters}/cytoscape.js +0 -0
  185. /package/dist/{graph-adapters → introspection/graph-adapters}/dot.js +0 -0
  186. /package/dist/{binding-scope.d.ts → resolution/binding-scope.d.ts} +0 -0
  187. /package/dist/{binding-scope.js → resolution/binding-scope.js} +0 -0
  188. /package/dist/{binding-select.d.ts → resolution/binding-select.d.ts} +0 -0
  189. /package/dist/{constraints.d.ts → resolution/constraints.d.ts} +0 -0
  190. /package/dist/{constraints.js → resolution/constraints.js} +0 -0
  191. /package/dist/{resolve-options.d.ts → resolution/resolve-options.d.ts} +0 -0
  192. /package/dist/{resolve-options.js → resolution/resolve-options.js} +0 -0
  193. /package/src/{binding-scope.ts → resolution/binding-scope.ts} +0 -0
  194. /package/src/{constraints.ts → resolution/constraints.ts} +0 -0
  195. /package/src/{resolve-options.ts → resolution/resolve-options.ts} +0 -0
@@ -0,0 +1,825 @@
1
+ import type { Binding, BindToBuilder } from "#/binding";
2
+ import { NO_INSTANCE } from "#/binding";
3
+ import type { BindingRegistration } from "#/container/binding-builders";
4
+ import { BindingChain } from "#/container/binding-builders";
5
+ import type { AutoRegisterRegistry } from "#/decorators/injectable";
6
+ import {
7
+ AsyncModuleLoadError,
8
+ CircularDependencyError,
9
+ DisposedContainerError,
10
+ InternalError,
11
+ RebindUnboundTokenError,
12
+ ScopeViolationError,
13
+ SyncDisposalNotSupportedError,
14
+ } from "#/errors";
15
+ import type { ContainerGraphJson, GraphOptions } from "#/introspection/dependency-graph";
16
+ import { buildDependencyGraph } from "#/introspection/dependency-graph";
17
+ import type { BindingSnapshot, ContainerSnapshot } from "#/introspection/inspector";
18
+ import { Inspector } from "#/introspection/inspector";
19
+ import { MetadataReaderToken } from "#/metadata/metadata-reader-token";
20
+ import type { MetadataReader } from "#/metadata/metadata-types";
21
+ import { defaultMetadataReader } from "#/metadata/symbol-metadata-reader";
22
+ import type { AsyncModule, ModuleBuilder, SyncModule } from "#/module";
23
+ import type { AsyncModuleBuilder } from "#/module";
24
+ import { isSyncModule, MODULE_SETUP } from "#/module";
25
+ import { BindingRegistry } from "#/registry";
26
+ import { effectiveBindingScope } from "#/resolution/binding-scope";
27
+ import type { ResolutionDiagnostics } from "#/resolution/diagnostics";
28
+ import { RESOLUTION_DIAGNOSTICS } from "#/resolution/diagnostics";
29
+ import { LifecycleManager } from "#/resolution/lifecycle";
30
+ import { injectionSlotToResolveOptions, bindingSlotToResolveOptions } from "#/resolution/resolve-options";
31
+ import { DependencyResolver } from "#/resolution/resolver";
32
+ import { ScopeManager } from "#/resolution/scope";
33
+ import type { Token } from "#/token";
34
+ import { tokenName } from "#/token";
35
+ import type {
36
+ ActivationHandler,
37
+ BindingIdentifier,
38
+ BindingScope,
39
+ Constructor,
40
+ DeactivationHandler,
41
+ ResolveOptions,
42
+ } from "#/types";
43
+
44
+ // ── Container interface ────────────────────────────────────────────────────────
45
+
46
+ /**
47
+ * @since 0.3.16-canary.0
48
+ */
49
+ export interface Container {
50
+ readonly isDisposed: boolean;
51
+
52
+ bind<const Value>(token: Token<Value> | Constructor<Value>): BindToBuilder<Value>;
53
+ unbind(tokenOrId: Token<unknown> | Constructor | BindingIdentifier): void;
54
+ unbindAsync(tokenOrId: Token<unknown> | Constructor | BindingIdentifier): Promise<void>;
55
+ unbindAll(): void;
56
+ unbindAllAsync(): Promise<void>;
57
+ rebind<const Value>(token: Token<Value> | Constructor<Value>): BindToBuilder<Value>;
58
+
59
+ load(...modules: Array<SyncModule>): void;
60
+ loadAsync(...modules: Array<SyncModule | AsyncModule>): Promise<void>;
61
+ unload(...modules: Array<SyncModule>): void;
62
+ unloadAsync(...modules: Array<SyncModule | AsyncModule>): Promise<void>;
63
+ loadAutoRegistered(registry: AutoRegisterRegistry): number;
64
+
65
+ onActivation<const Value>(token: Token<Value> | Constructor<Value>, handler: ActivationHandler<Value>): void;
66
+ onDeactivation<const Value>(token: Token<Value> | Constructor<Value>, handler: DeactivationHandler<Value>): void;
67
+
68
+ resolve<const Value>(token: Token<Value> | Constructor<Value>, options?: ResolveOptions): Value;
69
+ resolveAsync<const Value>(token: Token<Value> | Constructor<Value>, options?: ResolveOptions): Promise<Value>;
70
+ resolveOptional<const Value>(token: Token<Value> | Constructor<Value>, options?: ResolveOptions): Value | undefined;
71
+ resolveOptionalAsync<const Value>(
72
+ token: Token<Value> | Constructor<Value>,
73
+ options?: ResolveOptions,
74
+ ): Promise<Value | undefined>;
75
+ resolveAll<const Value>(token: Token<Value> | Constructor<Value>, options?: ResolveOptions): Array<Value>;
76
+ resolveAllAsync<const Value>(
77
+ token: Token<Value> | Constructor<Value>,
78
+ options?: ResolveOptions,
79
+ ): Promise<Array<Value>>;
80
+
81
+ createChild(): Container;
82
+
83
+ dispose(): Promise<void>;
84
+ [Symbol.asyncDispose](): Promise<void>;
85
+ [Symbol.dispose](): never;
86
+
87
+ initializeAsync(): Promise<void>;
88
+ validate(): void;
89
+
90
+ has(token: Token<unknown> | Constructor, options?: ResolveOptions): boolean;
91
+ hasOwn(token: Token<unknown> | Constructor, options?: ResolveOptions): boolean;
92
+ lookupBindings<const Value>(token: Token<Value> | Constructor<Value>): ReadonlyArray<BindingSnapshot>;
93
+ inspect(): ContainerSnapshot;
94
+ generateDependencyGraph(options?: GraphOptions): ContainerGraphJson;
95
+ }
96
+
97
+ /**
98
+ * @since 0.3.16-canary.0
99
+ */
100
+ export interface ContainerStatic {
101
+ create(): Container;
102
+ fromModules(...modules: Array<SyncModule>): Container;
103
+ fromModulesAsync(...modules: Array<SyncModule | AsyncModule>): Promise<Container>;
104
+ }
105
+
106
+ // ── DefaultContainer ──────────────────────────────────────────────────────────
107
+
108
+ class DefaultContainer implements Container {
109
+ #disposed = false;
110
+ readonly #registry: BindingRegistry;
111
+ readonly #scope: ScopeManager;
112
+ readonly #lifecycle: LifecycleManager;
113
+ #resolver!: DependencyResolver;
114
+ // Built on the first introspecting call — a container that only binds and resolves never needs it.
115
+ #inspector: Inspector | undefined;
116
+ readonly #parent: DefaultContainer | undefined;
117
+
118
+ // Module tracking: module -> ref count. Both tables stay unallocated until a module is loaded.
119
+ #moduleRefs: Map<object, number> | undefined;
120
+ // Module bindings: module -> array of binding IDs registered by it
121
+ #moduleBindingIds: Map<object, Array<BindingIdentifier>> | undefined;
122
+ // One shared registration for every chain this container's own `bind()` creates.
123
+ #registration: BindingRegistration | undefined;
124
+
125
+ constructor(parent?: DefaultContainer) {
126
+ this.#parent = parent;
127
+ this.#registry = new BindingRegistry();
128
+ this.#scope = new ScopeManager(parent !== undefined);
129
+ this.#lifecycle = new LifecycleManager();
130
+ this.#initResolver();
131
+ }
132
+
133
+ #getInspector(): Inspector {
134
+ return (this.#inspector ??= new Inspector(
135
+ this.#registry,
136
+ this.#scope,
137
+ this.#parent !== undefined,
138
+ () => this.#disposed,
139
+ ));
140
+ }
141
+
142
+ [RESOLUTION_DIAGNOSTICS](): ResolutionDiagnostics {
143
+ const builtSubsystems: Array<string> = [];
144
+ if (this.#inspector !== undefined) {
145
+ builtSubsystems.push("container.inspector");
146
+ }
147
+ if (this.#moduleRefs !== undefined || this.#moduleBindingIds !== undefined) {
148
+ builtSubsystems.push("container.moduleTables");
149
+ }
150
+ if (this.#registry.isBuilt) {
151
+ builtSubsystems.push("registry.namedIndex");
152
+ }
153
+ if (this.#scope.isBuilt) {
154
+ builtSubsystems.push("scope.scoped");
155
+ }
156
+ if (this.#lifecycle.isBuilt) {
157
+ builtSubsystems.push("lifecycle.activationHooks");
158
+ }
159
+ return { ...this.#resolver.describeCaches(), builtSubsystems };
160
+ }
161
+
162
+ #initResolver(): void {
163
+ const metadataReader = this.#getMetadataReader();
164
+ const parentResolver = this.#parent === undefined ? undefined : this.#parent.#resolver;
165
+ this.#resolver = new DependencyResolver(
166
+ this.#registry,
167
+ this.#scope,
168
+ this.#lifecycle,
169
+ metadataReader,
170
+ this,
171
+ parentResolver,
172
+ );
173
+ }
174
+
175
+ #getMetadataReader(): MetadataReader {
176
+ // Check if a custom MetadataReader has been bound
177
+ const metaBindings = this.#registry.getAll(MetadataReaderToken);
178
+ if (metaBindings.length > 0) {
179
+ try {
180
+ return this.#resolver.resolve(MetadataReaderToken, undefined, [], []);
181
+ } catch {
182
+ // fall through to default
183
+ }
184
+ }
185
+ if (this.#parent !== undefined) {
186
+ return this.#parent.#getMetadataReader();
187
+ }
188
+ return defaultMetadataReader;
189
+ }
190
+
191
+ get isDisposed(): boolean {
192
+ return this.#disposed;
193
+ }
194
+
195
+ // ── Binding ──────────────────────────────────────────────────────────────
196
+
197
+ bind<const Value>(token: Token<Value> | Constructor<Value>): BindToBuilder<Value> {
198
+ this.#assertNotDisposed();
199
+ return this.#createBindToBuilder(token);
200
+ }
201
+
202
+ /** The registration every non-module chain shares, so `bind()` allocates only the builder. */
203
+ #ownRegistration(): BindingRegistration {
204
+ return (this.#registration ??= { registry: this.#registry, moduleBindingIds: undefined });
205
+ }
206
+
207
+ /** One registration per module load, holding that module's id list directly. */
208
+ #moduleRegistration(moduleRef: object): BindingRegistration {
209
+ return {
210
+ registry: this.#registry,
211
+ moduleBindingIds: (this.#moduleBindingIds ??= new Map()).getOrInsert(moduleRef, []),
212
+ };
213
+ }
214
+
215
+ #createBindToBuilder<const Value>(
216
+ token: Token<Value> | Constructor<Value>,
217
+ registration: BindingRegistration = this.#ownRegistration(),
218
+ ): BindToBuilder<Value> {
219
+ return new BindingChain<Value>(token, registration);
220
+ }
221
+
222
+ unbind(tokenOrId: Token<unknown> | Constructor | BindingIdentifier): void {
223
+ this.#assertNotDisposed();
224
+ this.#unbindSync(tokenOrId);
225
+ }
226
+
227
+ /** Remove bindings from registry + scope and collect [binding, instance] pairs for deactivation. */
228
+ #collectDeactivationPairs(tokenOrId: Token<unknown> | Constructor | BindingIdentifier): Array<[Binding, unknown]> {
229
+ if (typeof tokenOrId === "string") {
230
+ const binding = this.#registry.removeById(tokenOrId);
231
+ return binding === undefined ? [] : this.#drainSingletons([binding]);
232
+ }
233
+ // Dropping the whole token in one pass: removing each binding by id instead would re-scan and
234
+ // re-index the token's binding list once per binding.
235
+ return this.#drainSingletons(this.#registry.removeByToken(tokenOrId));
236
+ }
237
+
238
+ /** Drain singleton scope entries for a set of already-removed bindings. */
239
+ #drainSingletons(bindings: ReadonlyArray<Binding>): Array<[Binding, unknown]> {
240
+ const pairs: Array<[Binding, unknown]> = [];
241
+ for (const binding of bindings) {
242
+ if (binding.instance !== NO_INSTANCE) {
243
+ pairs.push([binding, binding.instance]);
244
+ this.#scope.deleteSingleton(binding);
245
+ }
246
+ }
247
+ return pairs;
248
+ }
249
+
250
+ #unbindSync(tokenOrId: Token<unknown> | Constructor | BindingIdentifier): void {
251
+ const reader = this.#getMetadataReader();
252
+ for (const [binding, instance] of this.#collectDeactivationPairs(tokenOrId)) {
253
+ this.#lifecycle.runDeactivationSync(binding, instance, reader);
254
+ }
255
+ }
256
+
257
+ async unbindAsync(tokenOrId: Token<unknown> | Constructor | BindingIdentifier): Promise<void> {
258
+ this.#assertNotDisposed();
259
+ const reader = this.#getMetadataReader();
260
+ for (const [binding, instance] of this.#collectDeactivationPairs(tokenOrId)) {
261
+ await this.#lifecycle.runDeactivation(binding, instance, reader);
262
+ }
263
+ }
264
+
265
+ unbindAll(): void {
266
+ this.#assertNotDisposed();
267
+ const reader = this.#getMetadataReader();
268
+ for (const [binding, instance] of this.#drainSingletons(this.#registry.clear())) {
269
+ this.#lifecycle.runDeactivationSync(binding, instance, reader);
270
+ }
271
+ }
272
+
273
+ async unbindAllAsync(): Promise<void> {
274
+ this.#assertNotDisposed();
275
+ const reader = this.#getMetadataReader();
276
+ for (const [binding, instance] of this.#drainSingletons(this.#registry.clear())) {
277
+ await this.#lifecycle.runDeactivation(binding, instance, reader);
278
+ }
279
+ }
280
+
281
+ rebind<const Value>(token: Token<Value> | Constructor<Value>): BindToBuilder<Value> {
282
+ this.#assertNotDisposed();
283
+ if (!this.#registry.has(token)) {
284
+ throw new RebindUnboundTokenError(tokenName(token));
285
+ }
286
+ // Unbind existing (sync — if async deactivation, will throw AsyncDeactivationError)
287
+ this.#unbindSync(token);
288
+ return this.#createBindToBuilder(token);
289
+ }
290
+
291
+ // ── Module ────────────────────────────────────────────────────────────────
292
+
293
+ load(...modules: Array<SyncModule>): void {
294
+ this.#assertNotDisposed();
295
+ this.#loadSyncModules(modules);
296
+ }
297
+
298
+ #loadSyncModules(modules: Array<SyncModule>): void {
299
+ // Collect all modules in topological order (dedup by identity)
300
+ const toLoad = this.#collectModuleDeps(modules);
301
+ for (const module of toLoad) {
302
+ if (!isSyncModule(module)) {
303
+ throw new AsyncModuleLoadError(module.name);
304
+ }
305
+ const moduleRef = module as object;
306
+ const moduleRefs = (this.#moduleRefs ??= new Map());
307
+ const existing = moduleRefs.get(moduleRef);
308
+ if (existing !== undefined) {
309
+ moduleRefs.set(moduleRef, existing + 1);
310
+ continue;
311
+ }
312
+ moduleRefs.set(moduleRef, 1);
313
+ const builder = this.#createModuleBuilder(moduleRef);
314
+ module[MODULE_SETUP](builder);
315
+ }
316
+ }
317
+
318
+ #collectModuleDeps(modules: Array<SyncModule | AsyncModule>): Array<SyncModule | AsyncModule> {
319
+ const seen = new Set<object>();
320
+ const result: Array<SyncModule | AsyncModule> = [];
321
+
322
+ const visit = (module: SyncModule | AsyncModule): void => {
323
+ const moduleRef = module as object;
324
+ if (seen.has(moduleRef)) {
325
+ return;
326
+ }
327
+ seen.add(moduleRef);
328
+ // We'll collect deps during setup via the builder's import()
329
+ result.push(module);
330
+ };
331
+
332
+ for (const module of modules) {
333
+ visit(module);
334
+ }
335
+ return result;
336
+ }
337
+
338
+ async loadAsync(...modules: Array<SyncModule | AsyncModule>): Promise<void> {
339
+ this.#assertNotDisposed();
340
+ for (const module of modules) {
341
+ await this.#loadOneModuleAsync(module);
342
+ }
343
+ }
344
+
345
+ async #loadOneModuleAsync(module: SyncModule | AsyncModule): Promise<void> {
346
+ const moduleRef = module as object;
347
+ const moduleRefs = (this.#moduleRefs ??= new Map());
348
+ const existing = moduleRefs.get(moduleRef);
349
+ if (existing !== undefined) {
350
+ moduleRefs.set(moduleRef, existing + 1);
351
+ return;
352
+ }
353
+ moduleRefs.set(moduleRef, 1);
354
+
355
+ if (isSyncModule(module)) {
356
+ const builder = this.#createModuleBuilder(moduleRef);
357
+ module[MODULE_SETUP](builder);
358
+ } else {
359
+ const importPromises: Array<Promise<void>> = [];
360
+ const builder = this.#createAsyncModuleBuilder(moduleRef, importPromises);
361
+ await module[MODULE_SETUP](builder);
362
+ // Await nested async imports triggered inside the setup callback
363
+ if (importPromises.length > 0) {
364
+ await Promise.all(importPromises);
365
+ }
366
+ }
367
+ }
368
+
369
+ #createModuleBuilder(moduleRef: object): ModuleBuilder {
370
+ const registration = this.#moduleRegistration(moduleRef);
371
+ return {
372
+ bind: <const Value>(token: Token<Value> | Constructor<Value>): BindToBuilder<Value> =>
373
+ this.#createBindToBuilder(token, registration),
374
+ import: (...modules: Array<SyncModule>): void => {
375
+ this.#loadSyncModules(modules);
376
+ },
377
+ };
378
+ }
379
+
380
+ #createAsyncModuleBuilder(moduleRef: object, importPromises: Array<Promise<void>>): AsyncModuleBuilder {
381
+ const registration = this.#moduleRegistration(moduleRef);
382
+ return {
383
+ bind: <const Value>(token: Token<Value> | Constructor<Value>): BindToBuilder<Value> =>
384
+ this.#createBindToBuilder(token, registration),
385
+ import: (...modules: Array<SyncModule | AsyncModule>): void => {
386
+ for (const module of modules) {
387
+ importPromises.push(this.#loadOneModuleAsync(module));
388
+ }
389
+ },
390
+ };
391
+ }
392
+
393
+ unload(...modules: Array<SyncModule>): void {
394
+ this.#assertNotDisposed();
395
+ for (const module of modules) {
396
+ this.#unloadModuleSync(module as object);
397
+ }
398
+ }
399
+
400
+ /** Unregister module bindings and collect [binding, instance] pairs for deactivation. */
401
+ #removeModuleBindings(ref: object): Array<[Binding, unknown]> {
402
+ this.#moduleRefs?.delete(ref);
403
+ const ids = this.#moduleBindingIds?.get(ref) ?? [];
404
+ this.#moduleBindingIds?.delete(ref);
405
+ const pairs: Array<[Binding, unknown]> = [];
406
+ for (const id of ids) {
407
+ const binding = this.#registry.getById(id);
408
+ if (binding !== undefined) {
409
+ this.#registry.removeById(id);
410
+ if (binding.instance !== NO_INSTANCE) {
411
+ pairs.push([binding, binding.instance]);
412
+ this.#scope.deleteSingleton(binding);
413
+ }
414
+ }
415
+ }
416
+ return pairs;
417
+ }
418
+
419
+ #unloadModuleSync(ref: object): void {
420
+ const count = this.#moduleRefs?.get(ref) ?? 0;
421
+ if (count <= 1) {
422
+ const reader = this.#getMetadataReader();
423
+ for (const [binding, instance] of this.#removeModuleBindings(ref)) {
424
+ this.#lifecycle.runDeactivationSync(binding, instance, reader);
425
+ }
426
+ } else {
427
+ this.#moduleRefs!.set(ref, count - 1);
428
+ }
429
+ }
430
+
431
+ async unloadAsync(...modules: Array<SyncModule | AsyncModule>): Promise<void> {
432
+ this.#assertNotDisposed();
433
+ for (const module of modules) {
434
+ await this.#unloadModuleAsync(module as object);
435
+ }
436
+ }
437
+
438
+ async #unloadModuleAsync(ref: object): Promise<void> {
439
+ const count = this.#moduleRefs?.get(ref) ?? 0;
440
+ if (count <= 1) {
441
+ const reader = this.#getMetadataReader();
442
+ for (const [binding, instance] of this.#removeModuleBindings(ref)) {
443
+ await this.#lifecycle.runDeactivation(binding, instance, reader);
444
+ }
445
+ } else {
446
+ this.#moduleRefs!.set(ref, count - 1);
447
+ }
448
+ }
449
+
450
+ loadAutoRegistered(registry: AutoRegisterRegistry): number {
451
+ this.#assertNotDisposed();
452
+ const entries = registry.entries();
453
+ for (const { target, scope } of entries) {
454
+ const builder = this.#createBindToBuilder(target);
455
+ const bindingBuilder = builder.toSelf();
456
+ if (scope === "singleton") {
457
+ bindingBuilder.singleton();
458
+ } else if (scope === "scoped") {
459
+ bindingBuilder.scoped();
460
+ } else {
461
+ bindingBuilder.transient();
462
+ }
463
+ }
464
+ return entries.length;
465
+ }
466
+
467
+ // ── Lifecycle hooks ────────────────────────────────────────────────────────
468
+
469
+ onActivation<const Value>(token: Token<Value> | Constructor<Value>, handler: ActivationHandler<Value>): void {
470
+ this.#assertNotDisposed();
471
+ this.#lifecycle.registerActivation(token, handler);
472
+ }
473
+
474
+ onDeactivation<const Value>(token: Token<Value> | Constructor<Value>, handler: DeactivationHandler<Value>): void {
475
+ this.#assertNotDisposed();
476
+ this.#lifecycle.registerDeactivation(token, handler);
477
+ }
478
+
479
+ // ── Resolution ────────────────────────────────────────────────────────────
480
+
481
+ resolve<const Value>(token: Token<Value> | Constructor<Value>, options?: ResolveOptions): Value {
482
+ this.#assertNotDisposed();
483
+ if (options === undefined) {
484
+ return this.#resolver.resolveFromContext(token, [], []);
485
+ }
486
+ return this.#resolver.resolve(token, options, [], []);
487
+ }
488
+
489
+ resolveAsync<const Value>(token: Token<Value> | Constructor<Value>, options?: ResolveOptions): Promise<Value> {
490
+ this.#assertNotDisposed();
491
+ if (options === undefined) {
492
+ return this.#resolver.resolveAsyncFromContext(token, [], []);
493
+ }
494
+ return this.#resolver.resolveAsync(token, options, [], []);
495
+ }
496
+
497
+ resolveOptional<const Value>(token: Token<Value> | Constructor<Value>, options?: ResolveOptions): Value | undefined {
498
+ this.#assertNotDisposed();
499
+ return this.#resolver.resolveOptional(token, options, [], []);
500
+ }
501
+
502
+ resolveOptionalAsync<const Value>(
503
+ token: Token<Value> | Constructor<Value>,
504
+ options?: ResolveOptions,
505
+ ): Promise<Value | undefined> {
506
+ this.#assertNotDisposed();
507
+ return this.#resolver.resolveOptionalAsync(token, options, [], []);
508
+ }
509
+
510
+ resolveAll<const Value>(token: Token<Value> | Constructor<Value>, options?: ResolveOptions): Array<Value> {
511
+ this.#assertNotDisposed();
512
+ return this.#resolver.resolveAll(token, options, [], []);
513
+ }
514
+
515
+ resolveAllAsync<const Value>(
516
+ token: Token<Value> | Constructor<Value>,
517
+ options?: ResolveOptions,
518
+ ): Promise<Array<Value>> {
519
+ this.#assertNotDisposed();
520
+ return this.#resolver.resolveAllAsync(token, options, [], []);
521
+ }
522
+
523
+ // ── Child ─────────────────────────────────────────────────────────────────
524
+
525
+ createChild(): Container {
526
+ this.#assertNotDisposed();
527
+ return new DefaultContainer(this);
528
+ }
529
+
530
+ // ── Dispose ───────────────────────────────────────────────────────────────
531
+
532
+ async dispose(): Promise<void> {
533
+ if (this.#disposed) {
534
+ return;
535
+ }
536
+ this.#disposed = true;
537
+
538
+ // Deactivate all singletons in this container (own only)
539
+ const reader = this.#getMetadataReader();
540
+ // Iterate a copy: a deactivation handler is user code, and the live list is what
541
+ // materializing or dropping a singleton mutates.
542
+ for (const binding of this.#scope.cachedSingletons().slice()) {
543
+ await this.#lifecycle.runDeactivation(binding, binding.instance, reader);
544
+ }
545
+
546
+ this.#scope.clearAll();
547
+ }
548
+
549
+ [Symbol.asyncDispose](): Promise<void> {
550
+ return this.dispose();
551
+ }
552
+
553
+ [Symbol.dispose](): never {
554
+ throw new SyncDisposalNotSupportedError();
555
+ }
556
+
557
+ // ── Initialization ────────────────────────────────────────────────────────
558
+
559
+ async initializeAsync(): Promise<void> {
560
+ this.#assertNotDisposed();
561
+ const allBindings = this.#registry.allBindings();
562
+ for (const binding of allBindings) {
563
+ if (binding.kind === "alias") {
564
+ continue;
565
+ }
566
+ const scope = effectiveBindingScope(binding);
567
+ if (scope === "singleton" && binding.instance === NO_INSTANCE) {
568
+ if (binding.predicate !== undefined) {
569
+ continue;
570
+ }
571
+ // Has activation — need to resolve
572
+ if (binding.kind === "constant" && binding.onActivation === undefined) {
573
+ continue;
574
+ }
575
+ const slotOptions = bindingSlotToResolveOptions(binding.slot);
576
+ await this.resolveAsync(binding.token as Token<unknown>, slotOptions);
577
+ }
578
+ }
579
+ }
580
+
581
+ // ── Validate ──────────────────────────────────────────────────────────────
582
+
583
+ validate(): void {
584
+ this.#assertNotDisposed();
585
+ const reader = this.#getMetadataReader();
586
+ const allBindings = this.#registry.allBindings();
587
+
588
+ for (const binding of allBindings) {
589
+ if (!this.#isSingletonStaticAnalyzableBinding(binding)) {
590
+ continue;
591
+ }
592
+ this.#validateSingletonBindingGraph(binding, reader);
593
+ }
594
+ }
595
+
596
+ #isSingletonStaticAnalyzableBinding(binding: Binding): boolean {
597
+ if (effectiveBindingScope(binding) !== "singleton") {
598
+ return false;
599
+ }
600
+ return binding.kind === "class" || binding.kind === "resolved" || binding.kind === "resolved-async";
601
+ }
602
+
603
+ /**
604
+ * DFS over explicit constructor / `toResolved*` dependency edges. Follows `toAlias` chains to the
605
+ * terminal binding for scope checks (SPEC §6.9).
606
+ *
607
+ * @remarks A `toDynamic*` dependency is scope-checked like any other — its declared scope is what
608
+ * makes it captive — but the DFS does not descend into the factory, whose body is opaque.
609
+ */
610
+ #validateSingletonBindingGraph(root: Binding, reader: MetadataReader): void {
611
+ const rootName = tokenName(root.token as Token<unknown>);
612
+
613
+ const dfs = (current: Binding, pathNames: Array<string>, pathBindingIds: Set<BindingIdentifier>): void => {
614
+ if (pathBindingIds.has(current.id)) {
615
+ return;
616
+ }
617
+ const extendedPathIds = new Set(pathBindingIds);
618
+ extendedPathIds.add(current.id);
619
+
620
+ for (const edge of this.#collectStaticDependencyEdges(current, reader)) {
621
+ const { terminal, depTokenName } = edge;
622
+ const depScope = this.#validationScopeFromTerminal(terminal);
623
+ if (depScope === "opaque") {
624
+ continue;
625
+ }
626
+ if (depScope === "scoped" || depScope === "transient") {
627
+ throw new ScopeViolationError({
628
+ consumerToken: rootName,
629
+ consumerScope: "singleton",
630
+ dependencyToken: depTokenName,
631
+ dependencyScope: depScope,
632
+ path: [...pathNames, depTokenName],
633
+ });
634
+ }
635
+ if (terminal.kind === "class" || terminal.kind === "resolved" || terminal.kind === "resolved-async") {
636
+ dfs(terminal, [...pathNames, depTokenName], extendedPathIds);
637
+ }
638
+ }
639
+ };
640
+
641
+ dfs(root, [rootName], new Set());
642
+ }
643
+
644
+ #validationScopeFromTerminal(terminal: Binding): BindingScope | "opaque" {
645
+ switch (terminal.kind) {
646
+ case "constant":
647
+ return "singleton";
648
+ // A factory's *body* is not statically analyzable, but the scope it was bound with is
649
+ // declared like any other — so the captive-dependency check applies. The DFS below still
650
+ // refuses to descend into the factory; only this edge is judged.
651
+ case "dynamic":
652
+ case "dynamic-async":
653
+ return terminal.scope;
654
+ case "class":
655
+ case "resolved":
656
+ case "resolved-async":
657
+ return terminal.scope;
658
+ case "alias":
659
+ throw new InternalError("validate: expected terminal binding after alias resolution");
660
+ default: {
661
+ const exhaustive: never = terminal;
662
+ return exhaustive;
663
+ }
664
+ }
665
+ }
666
+
667
+ #followAliasChainToTerminal(binding: Binding, options: ResolveOptions | undefined): Binding | undefined {
668
+ const cyclePath: Array<string> = [];
669
+ const seenAliasIds = new Set<BindingIdentifier>();
670
+ let current: Binding | undefined = binding;
671
+
672
+ while (current !== undefined && current.kind === "alias") {
673
+ if (seenAliasIds.has(current.id)) {
674
+ throw new CircularDependencyError(cyclePath);
675
+ }
676
+ seenAliasIds.add(current.id);
677
+ cyclePath.push(tokenName(current.token as Token<unknown>));
678
+ const nextToken = current.target as Token<unknown> | Constructor;
679
+ const next = this.#resolver.peekBindingForValidate(nextToken, options);
680
+ if (next === undefined) {
681
+ return undefined;
682
+ }
683
+ current = next.binding;
684
+ }
685
+ return current;
686
+ }
687
+
688
+ #collectStaticDependencyEdges(
689
+ binding: Binding,
690
+ reader: MetadataReader,
691
+ ): Array<{ terminal: Binding; depTokenName: string }> {
692
+ const edges: Array<{ terminal: Binding; depTokenName: string }> = [];
693
+
694
+ const pushTerminal = (terminal: Binding | undefined, displayName: string): void => {
695
+ if (terminal === undefined) {
696
+ return;
697
+ }
698
+ edges.push({ terminal, depTokenName: displayName });
699
+ };
700
+
701
+ if (binding.kind === "class") {
702
+ const meta = reader.getConstructorMetadata(binding.target as Constructor);
703
+ if (meta === undefined) {
704
+ return edges;
705
+ }
706
+ for (const param of meta.params) {
707
+ const paramOptions = injectionSlotToResolveOptions(param);
708
+ if (param.optional) {
709
+ continue;
710
+ }
711
+ const tokenRef = param.token;
712
+ if (param.multi) {
713
+ const candidates = this.#resolver.peekCandidateBindingsForValidate(tokenRef, paramOptions);
714
+ for (const cand of candidates) {
715
+ const term = this.#followAliasChainToTerminal(cand, paramOptions);
716
+ pushTerminal(term, term !== undefined ? tokenName(term.token as Token<unknown>) : "");
717
+ }
718
+ continue;
719
+ }
720
+ const found =
721
+ paramOptions === undefined
722
+ ? this.#resolver.peekBindingForValidate(tokenRef, undefined)
723
+ : this.#resolver.peekBindingForValidate(tokenRef, paramOptions);
724
+ if (found === undefined) {
725
+ continue;
726
+ }
727
+ const term = this.#followAliasChainToTerminal(found.binding, paramOptions);
728
+ pushTerminal(term, term !== undefined ? tokenName(term.token as Token<unknown>) : "");
729
+ }
730
+ return edges;
731
+ }
732
+
733
+ if (binding.kind === "resolved" || binding.kind === "resolved-async") {
734
+ for (const dep of binding.deps) {
735
+ const depOptions = injectionSlotToResolveOptions(dep);
736
+ if (dep.optional) {
737
+ continue;
738
+ }
739
+ const tokenRef = dep.token as Token<unknown> | Constructor;
740
+ if (dep.multi) {
741
+ const candidates = this.#resolver.peekCandidateBindingsForValidate(tokenRef, depOptions);
742
+ for (const cand of candidates) {
743
+ const term = this.#followAliasChainToTerminal(cand, depOptions);
744
+ pushTerminal(term, term !== undefined ? tokenName(term.token as Token<unknown>) : "");
745
+ }
746
+ continue;
747
+ }
748
+ const found =
749
+ depOptions === undefined
750
+ ? this.#resolver.peekBindingForValidate(tokenRef, undefined)
751
+ : this.#resolver.peekBindingForValidate(tokenRef, depOptions);
752
+ if (found === undefined) {
753
+ continue;
754
+ }
755
+ const term = this.#followAliasChainToTerminal(found.binding, depOptions);
756
+ pushTerminal(term, term !== undefined ? tokenName(term.token as Token<unknown>) : "");
757
+ }
758
+ }
759
+
760
+ return edges;
761
+ }
762
+
763
+ // ── Introspection ─────────────────────────────────────────────────────────
764
+
765
+ has(token: Token<unknown> | Constructor, options?: ResolveOptions): boolean {
766
+ this.#assertNotDisposed();
767
+ return this.#getInspector().has(token, options, () => this.#parent?.has(token, options) ?? false);
768
+ }
769
+
770
+ hasOwn(token: Token<unknown> | Constructor, options?: ResolveOptions): boolean {
771
+ this.#assertNotDisposed();
772
+ return this.#getInspector().hasOwn(token, options);
773
+ }
774
+
775
+ lookupBindings<const Value>(token: Token<Value> | Constructor<Value>): ReadonlyArray<BindingSnapshot> {
776
+ this.#assertNotDisposed();
777
+ return this.#getInspector().lookupBindings(token);
778
+ }
779
+
780
+ inspect(): ContainerSnapshot {
781
+ this.#assertNotDisposed();
782
+ return this.#getInspector().inspect();
783
+ }
784
+
785
+ generateDependencyGraph(options?: GraphOptions): ContainerGraphJson {
786
+ this.#assertNotDisposed();
787
+ return buildDependencyGraph(
788
+ this.#registry,
789
+ this.#getMetadataReader(),
790
+ options,
791
+ this.#parent === undefined ? undefined : this.#parent.#registry,
792
+ );
793
+ }
794
+
795
+ // ── Internal ──────────────────────────────────────────────────────────────
796
+
797
+ #assertNotDisposed(): void {
798
+ if (this.#disposed) {
799
+ throw new DisposedContainerError();
800
+ }
801
+ }
802
+ }
803
+
804
+ // ── Container static ──────────────────────────────────────────────────────────
805
+
806
+ /**
807
+ * @since 0.3.16-canary.0
808
+ */
809
+ export const Container: ContainerStatic & { create(): Container } = {
810
+ create(): Container {
811
+ return new DefaultContainer();
812
+ },
813
+
814
+ fromModules(...modules: Array<SyncModule>): Container {
815
+ const container = new DefaultContainer();
816
+ container.load(...modules);
817
+ return container;
818
+ },
819
+
820
+ async fromModulesAsync(...modules: Array<SyncModule | AsyncModule>): Promise<Container> {
821
+ const container = new DefaultContainer();
822
+ await container.loadAsync(...modules);
823
+ return container;
824
+ },
825
+ };