@codefast/di 0.5.0-canary.7 → 0.5.0-canary.9

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 (114) hide show
  1. package/CHANGELOG.md +205 -0
  2. package/README.md +6 -2
  3. package/dist/binding.d.ts +85 -24
  4. package/dist/binding.d.ts.map +1 -1
  5. package/dist/binding.js +55 -0
  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 +32 -11
  10. package/dist/container/binding-builders.d.ts.map +1 -1
  11. package/dist/container/binding-builders.js +144 -192
  12. package/dist/container/binding-builders.js.map +1 -1
  13. package/dist/container/container.d.ts.map +1 -1
  14. package/dist/container/container.js +141 -201
  15. package/dist/container/container.js.map +1 -1
  16. package/dist/decorators/inject.d.ts +2 -4
  17. package/dist/decorators/inject.d.ts.map +1 -1
  18. package/dist/decorators/inject.js.map +1 -1
  19. package/dist/errors.d.ts +24 -0
  20. package/dist/errors.d.ts.map +1 -1
  21. package/dist/errors.js +30 -0
  22. package/dist/errors.js.map +1 -1
  23. package/dist/index.d.ts +1 -2
  24. package/dist/index.d.ts.map +1 -1
  25. package/dist/index.js +3 -2
  26. package/dist/index.js.map +1 -1
  27. package/dist/introspection/inspector.d.ts +0 -1
  28. package/dist/introspection/inspector.d.ts.map +1 -1
  29. package/dist/introspection/inspector.js +3 -8
  30. package/dist/introspection/inspector.js.map +1 -1
  31. package/dist/metadata/metadata-keys.d.ts +3 -6
  32. package/dist/metadata/metadata-keys.d.ts.map +1 -1
  33. package/dist/metadata/metadata-keys.js +3 -6
  34. package/dist/metadata/metadata-keys.js.map +1 -1
  35. package/dist/registry.d.ts +14 -2
  36. package/dist/registry.d.ts.map +1 -1
  37. package/dist/registry.js +81 -78
  38. package/dist/registry.js.map +1 -1
  39. package/dist/resolution/activation-need.d.ts +27 -0
  40. package/dist/resolution/activation-need.d.ts.map +1 -0
  41. package/dist/resolution/activation-need.js +68 -0
  42. package/dist/resolution/activation-need.js.map +1 -0
  43. package/dist/resolution/binding-lookup-cache.d.ts +41 -0
  44. package/dist/resolution/binding-lookup-cache.d.ts.map +1 -0
  45. package/dist/resolution/binding-lookup-cache.js +118 -0
  46. package/dist/resolution/binding-lookup-cache.js.map +1 -0
  47. package/dist/resolution/binding-scope.d.ts +5 -2
  48. package/dist/resolution/binding-scope.d.ts.map +1 -1
  49. package/dist/resolution/binding-scope.js +6 -17
  50. package/dist/resolution/binding-scope.js.map +1 -1
  51. package/dist/resolution/binding-select.d.ts +8 -1
  52. package/dist/resolution/binding-select.d.ts.map +1 -1
  53. package/dist/resolution/binding-select.js +14 -36
  54. package/dist/resolution/binding-select.js.map +1 -1
  55. package/dist/resolution/class-introspector.d.ts +27 -0
  56. package/dist/resolution/class-introspector.d.ts.map +1 -0
  57. package/dist/resolution/class-introspector.js +60 -0
  58. package/dist/resolution/class-introspector.js.map +1 -0
  59. package/dist/resolution/diagnostics.d.ts +41 -0
  60. package/dist/resolution/diagnostics.d.ts.map +1 -0
  61. package/dist/resolution/diagnostics.js +18 -0
  62. package/dist/resolution/diagnostics.js.map +1 -0
  63. package/dist/resolution/environment.d.ts +48 -1
  64. package/dist/resolution/environment.d.ts.map +1 -1
  65. package/dist/resolution/environment.js +134 -5
  66. package/dist/resolution/environment.js.map +1 -1
  67. package/dist/resolution/instantiation-plan.d.ts +15 -15
  68. package/dist/resolution/instantiation-plan.d.ts.map +1 -1
  69. package/dist/resolution/instantiation-plan.js +69 -49
  70. package/dist/resolution/instantiation-plan.js.map +1 -1
  71. package/dist/resolution/lifecycle.d.ts +2 -0
  72. package/dist/resolution/lifecycle.d.ts.map +1 -1
  73. package/dist/resolution/lifecycle.js +60 -62
  74. package/dist/resolution/lifecycle.js.map +1 -1
  75. package/dist/resolution/resolution-path.d.ts +84 -17
  76. package/dist/resolution/resolution-path.d.ts.map +1 -1
  77. package/dist/resolution/resolution-path.js +68 -23
  78. package/dist/resolution/resolution-path.js.map +1 -1
  79. package/dist/resolution/resolve-options.d.ts +41 -4
  80. package/dist/resolution/resolve-options.d.ts.map +1 -1
  81. package/dist/resolution/resolve-options.js +25 -1
  82. package/dist/resolution/resolve-options.js.map +1 -1
  83. package/dist/resolution/resolver.d.ts +33 -10
  84. package/dist/resolution/resolver.d.ts.map +1 -1
  85. package/dist/resolution/resolver.js +471 -830
  86. package/dist/resolution/resolver.js.map +1 -1
  87. package/dist/resolution/scope.d.ts +11 -15
  88. package/dist/resolution/scope.d.ts.map +1 -1
  89. package/dist/resolution/scope.js +55 -43
  90. package/dist/resolution/scope.js.map +1 -1
  91. package/package.json +10 -106
  92. package/src/binding.ts +146 -24
  93. package/src/constructor-type.ts +4 -5
  94. package/src/container/binding-builders.ts +184 -284
  95. package/src/container/container.ts +161 -221
  96. package/src/decorators/inject.ts +3 -5
  97. package/src/errors.ts +38 -0
  98. package/src/index.ts +4 -1
  99. package/src/introspection/inspector.ts +3 -9
  100. package/src/metadata/metadata-keys.ts +3 -6
  101. package/src/registry.ts +90 -94
  102. package/src/resolution/activation-need.ts +85 -0
  103. package/src/resolution/binding-lookup-cache.ts +148 -0
  104. package/src/resolution/binding-scope.ts +6 -17
  105. package/src/resolution/binding-select.ts +15 -39
  106. package/src/resolution/class-introspector.ts +74 -0
  107. package/src/resolution/diagnostics.ts +43 -0
  108. package/src/resolution/environment.ts +181 -5
  109. package/src/resolution/instantiation-plan.ts +116 -64
  110. package/src/resolution/lifecycle.ts +69 -62
  111. package/src/resolution/resolution-path.ts +122 -43
  112. package/src/resolution/resolve-options.ts +51 -4
  113. package/src/resolution/resolver.ts +649 -1081
  114. package/src/resolution/scope.ts +58 -47
@@ -1,6 +1,7 @@
1
1
  import type { Binding, BindToBuilder } from "#/binding";
2
- import { bindingSlotEquals } from "#/binding";
3
- import { BindingEntry } from "#/container/binding-builders";
2
+ import { NO_INSTANCE } from "#/binding";
3
+ import type { BindingRegistration } from "#/container/binding-builders";
4
+ import { BindingChain } from "#/container/binding-builders";
4
5
  import type { AutoRegisterRegistry } from "#/decorators/injectable";
5
6
  import {
6
7
  AsyncModuleLoadError,
@@ -23,7 +24,11 @@ import type { AsyncModuleBuilder } from "#/module";
23
24
  import { isSyncModule, MODULE_SETUP } from "#/module";
24
25
  import { BindingRegistry } from "#/registry";
25
26
  import { effectiveBindingScope } from "#/resolution/binding-scope";
27
+ import type { ResolutionDiagnostics } from "#/resolution/diagnostics";
28
+ import { RESOLUTION_DIAGNOSTICS } from "#/resolution/diagnostics";
26
29
  import { LifecycleManager } from "#/resolution/lifecycle";
30
+ import { ROOT_BRANCH } from "#/resolution/resolution-path";
31
+ import type { DependencySlot } from "#/resolution/resolve-options";
27
32
  import { injectionSlotToResolveOptions, bindingSlotToResolveOptions } from "#/resolution/resolve-options";
28
33
  import { DependencyResolver } from "#/resolution/resolver";
29
34
  import { ScopeManager } from "#/resolution/scope";
@@ -38,17 +43,6 @@ import type {
38
43
  ResolveOptions,
39
44
  } from "#/types";
40
45
 
41
- // True when re-adding `restored` would immediately be displaced again by `current`
42
- // (both slot-based with equal slots) — in that case the replacement was legitimate.
43
- function displacesRestoredBinding(current: Binding, restored: Binding): boolean {
44
- const currentIsPurePredicate =
45
- current.predicate !== undefined && current.slot.name === undefined && current.slot.tags.length === 0;
46
- if (currentIsPurePredicate) {
47
- return false;
48
- }
49
- return bindingSlotEquals(current.slot, restored.slot);
50
- }
51
-
52
46
  // ── Container interface ────────────────────────────────────────────────────────
53
47
 
54
48
  /**
@@ -119,23 +113,54 @@ class DefaultContainer implements Container {
119
113
  readonly #scope: ScopeManager;
120
114
  readonly #lifecycle: LifecycleManager;
121
115
  #resolver!: DependencyResolver;
122
- readonly #inspector: Inspector;
116
+ // Built on the first introspecting call — a container that only binds and resolves never needs it.
117
+ #inspector: Inspector | undefined;
123
118
  readonly #parent: DefaultContainer | undefined;
124
119
 
125
- // Module tracking: module -> ref count
126
- readonly #moduleRefs = new Map<object, number>();
120
+ // Module tracking: module -> ref count. Both tables stay unallocated until a module is loaded.
121
+ #moduleRefs: Map<object, number> | undefined;
127
122
  // Module bindings: module -> array of binding IDs registered by it
128
- readonly #moduleBindingIds = new Map<object, Array<BindingIdentifier>>();
123
+ #moduleBindingIds: Map<object, Array<BindingIdentifier>> | undefined;
124
+ // One shared registration for every chain this container's own `bind()` creates.
125
+ #registration: BindingRegistration | undefined;
129
126
 
130
127
  constructor(parent?: DefaultContainer) {
131
128
  this.#parent = parent;
132
129
  this.#registry = new BindingRegistry();
133
130
  this.#scope = new ScopeManager(parent !== undefined);
134
131
  this.#lifecycle = new LifecycleManager();
135
- this.#inspector = new Inspector(this.#registry, this.#scope, parent !== undefined, () => this.#disposed);
136
132
  this.#initResolver();
137
133
  }
138
134
 
135
+ #getInspector(): Inspector {
136
+ return (this.#inspector ??= new Inspector(
137
+ this.#registry,
138
+ this.#scope,
139
+ this.#parent !== undefined,
140
+ () => this.#disposed,
141
+ ));
142
+ }
143
+
144
+ [RESOLUTION_DIAGNOSTICS](): ResolutionDiagnostics {
145
+ const builtSubsystems: Array<string> = [];
146
+ if (this.#inspector !== undefined) {
147
+ builtSubsystems.push("container.inspector");
148
+ }
149
+ if (this.#moduleRefs !== undefined || this.#moduleBindingIds !== undefined) {
150
+ builtSubsystems.push("container.moduleTables");
151
+ }
152
+ if (this.#registry.isBuilt) {
153
+ builtSubsystems.push("registry.namedIndex");
154
+ }
155
+ if (this.#scope.isBuilt) {
156
+ builtSubsystems.push("scope.scoped");
157
+ }
158
+ if (this.#lifecycle.isBuilt) {
159
+ builtSubsystems.push("lifecycle.activationHooks");
160
+ }
161
+ return { ...this.#resolver.describeCaches(), scopedInstanceCount: this.#scope.scopedCount, builtSubsystems };
162
+ }
163
+
139
164
  #initResolver(): void {
140
165
  const metadataReader = this.#getMetadataReader();
141
166
  const parentResolver = this.#parent === undefined ? undefined : this.#parent.#resolver;
@@ -176,63 +201,24 @@ class DefaultContainer implements Container {
176
201
  return this.#createBindToBuilder(token);
177
202
  }
178
203
 
204
+ /** The registration every non-module chain shares, so `bind()` allocates only the builder. */
205
+ #ownRegistration(): BindingRegistration {
206
+ return (this.#registration ??= { registry: this.#registry, moduleBindingIds: undefined });
207
+ }
208
+
209
+ /** One registration per module load, holding that module's id list directly. */
210
+ #moduleRegistration(moduleRef: object): BindingRegistration {
211
+ return {
212
+ registry: this.#registry,
213
+ moduleBindingIds: (this.#moduleBindingIds ??= new Map()).getOrInsert(moduleRef, []),
214
+ };
215
+ }
216
+
179
217
  #createBindToBuilder<const Value>(
180
218
  token: Token<Value> | Constructor<Value>,
181
- moduleRef?: object,
219
+ registration: BindingRegistration = this.#ownRegistration(),
182
220
  ): BindToBuilder<Value> {
183
- const registry = this.#registry;
184
- // Bindings displaced by this fluent chain's commits — each stays restorable
185
- // until the chain settles on a shape that genuinely conflicts with it. A list,
186
- // because one chain can displace several bindings (the default at an
187
- // intermediate commit, then a named/tagged binding at the final one).
188
- const displacedByChain: Array<Binding> = [];
189
-
190
- const commitBinding = <BindingValue>(
191
- binding: Binding<BindingValue>,
192
- previousId?: BindingIdentifier,
193
- ): BindingIdentifier => {
194
- if (previousId !== undefined) {
195
- registry.removeById(previousId);
196
- if (moduleRef !== undefined) {
197
- const ids = this.#moduleBindingIds.get(moduleRef);
198
- if (ids !== undefined) {
199
- const idx = ids.indexOf(previousId);
200
- if (idx !== -1) {
201
- ids.splice(idx, 1);
202
- }
203
- }
204
- }
205
- }
206
- // The registry stores value-erased bindings — this is the single erasure point for the builder chain.
207
- const displaced = registry.add(binding as Binding);
208
- if (displaced !== undefined) {
209
- // A fluent chain commits eagerly on each refinement, so an intermediate
210
- // commit can displace a binding that the final shape would never conflict
211
- // with. Remember it so a later re-commit that morphs away (named/tagged
212
- // slot, pure predicate) can restore it.
213
- displacedByChain.push(displaced);
214
- }
215
- if (previousId !== undefined && displacedByChain.length > 0) {
216
- for (let index = displacedByChain.length - 1; index >= 0; index -= 1) {
217
- const candidate = displacedByChain[index]!;
218
- if (!displacesRestoredBinding(binding as Binding, candidate)) {
219
- registry.add(candidate);
220
- displacedByChain.splice(index, 1);
221
- }
222
- }
223
- }
224
- if (moduleRef !== undefined) {
225
- let ids = this.#moduleBindingIds.get(moduleRef);
226
- if (ids === undefined) {
227
- ids = [];
228
- this.#moduleBindingIds.set(moduleRef, ids);
229
- }
230
- ids.push(binding.id);
231
- }
232
- return binding.id;
233
- };
234
-
235
- return new BindingEntry<Value>(token, commitBinding);
221
+ return new BindingChain<Value>(token, registration);
236
222
  }
237
223
 
238
224
  unbind(tokenOrId: Token<unknown> | Constructor | BindingIdentifier): void {
@@ -251,14 +237,15 @@ class DefaultContainer implements Container {
251
237
  return this.#drainSingletons(this.#registry.removeByToken(tokenOrId));
252
238
  }
253
239
 
254
- /** Drain singleton scope entries for a set of already-removed bindings. */
240
+ /** Drain scope entries for already-removed bindings; only singletons yield deactivation pairs. */
255
241
  #drainSingletons(bindings: ReadonlyArray<Binding>): Array<[Binding, unknown]> {
256
242
  const pairs: Array<[Binding, unknown]> = [];
257
243
  for (const binding of bindings) {
258
- if (this.#scope.hasSingleton(binding.id)) {
259
- pairs.push([binding, this.#scope.getSingleton(binding.id)]);
260
- this.#scope.deleteSingleton(binding.id);
244
+ if (binding.instance !== NO_INSTANCE) {
245
+ pairs.push([binding, binding.instance]);
246
+ this.#scope.deleteSingleton(binding);
261
247
  }
248
+ this.#scope.deleteScoped(binding.id);
262
249
  }
263
250
  return pairs;
264
251
  }
@@ -311,45 +298,26 @@ class DefaultContainer implements Container {
311
298
  this.#loadSyncModules(modules);
312
299
  }
313
300
 
314
- #loadSyncModules(modules: Array<SyncModule>): void {
315
- // Collect all modules in topological order (dedup by identity)
316
- const toLoad = this.#collectModuleDeps(modules);
317
- for (const module of toLoad) {
301
+ // Imports nested inside a module's setup re-enter here through the builder, so a module listed
302
+ // twice in one call is deduped by identity and the rest is ref-counting.
303
+ #loadSyncModules(modules: ReadonlyArray<SyncModule | AsyncModule>): void {
304
+ for (const module of new Set(modules)) {
318
305
  if (!isSyncModule(module)) {
319
306
  throw new AsyncModuleLoadError(module.name);
320
307
  }
321
308
  const moduleRef = module as object;
322
- const existing = this.#moduleRefs.get(moduleRef);
309
+ const moduleRefs = (this.#moduleRefs ??= new Map());
310
+ const existing = moduleRefs.get(moduleRef);
323
311
  if (existing !== undefined) {
324
- this.#moduleRefs.set(moduleRef, existing + 1);
312
+ moduleRefs.set(moduleRef, existing + 1);
325
313
  continue;
326
314
  }
327
- this.#moduleRefs.set(moduleRef, 1);
315
+ moduleRefs.set(moduleRef, 1);
328
316
  const builder = this.#createModuleBuilder(moduleRef);
329
317
  module[MODULE_SETUP](builder);
330
318
  }
331
319
  }
332
320
 
333
- #collectModuleDeps(modules: Array<SyncModule | AsyncModule>): Array<SyncModule | AsyncModule> {
334
- const seen = new Set<object>();
335
- const result: Array<SyncModule | AsyncModule> = [];
336
-
337
- const visit = (module: SyncModule | AsyncModule): void => {
338
- const moduleRef = module as object;
339
- if (seen.has(moduleRef)) {
340
- return;
341
- }
342
- seen.add(moduleRef);
343
- // We'll collect deps during setup via the builder's import()
344
- result.push(module);
345
- };
346
-
347
- for (const module of modules) {
348
- visit(module);
349
- }
350
- return result;
351
- }
352
-
353
321
  async loadAsync(...modules: Array<SyncModule | AsyncModule>): Promise<void> {
354
322
  this.#assertNotDisposed();
355
323
  for (const module of modules) {
@@ -359,12 +327,13 @@ class DefaultContainer implements Container {
359
327
 
360
328
  async #loadOneModuleAsync(module: SyncModule | AsyncModule): Promise<void> {
361
329
  const moduleRef = module as object;
362
- const existing = this.#moduleRefs.get(moduleRef);
330
+ const moduleRefs = (this.#moduleRefs ??= new Map());
331
+ const existing = moduleRefs.get(moduleRef);
363
332
  if (existing !== undefined) {
364
- this.#moduleRefs.set(moduleRef, existing + 1);
333
+ moduleRefs.set(moduleRef, existing + 1);
365
334
  return;
366
335
  }
367
- this.#moduleRefs.set(moduleRef, 1);
336
+ moduleRefs.set(moduleRef, 1);
368
337
 
369
338
  if (isSyncModule(module)) {
370
339
  const builder = this.#createModuleBuilder(moduleRef);
@@ -381,9 +350,10 @@ class DefaultContainer implements Container {
381
350
  }
382
351
 
383
352
  #createModuleBuilder(moduleRef: object): ModuleBuilder {
353
+ const registration = this.#moduleRegistration(moduleRef);
384
354
  return {
385
355
  bind: <const Value>(token: Token<Value> | Constructor<Value>): BindToBuilder<Value> =>
386
- this.#createBindToBuilder(token, moduleRef),
356
+ this.#createBindToBuilder(token, registration),
387
357
  import: (...modules: Array<SyncModule>): void => {
388
358
  this.#loadSyncModules(modules);
389
359
  },
@@ -391,9 +361,10 @@ class DefaultContainer implements Container {
391
361
  }
392
362
 
393
363
  #createAsyncModuleBuilder(moduleRef: object, importPromises: Array<Promise<void>>): AsyncModuleBuilder {
364
+ const registration = this.#moduleRegistration(moduleRef);
394
365
  return {
395
366
  bind: <const Value>(token: Token<Value> | Constructor<Value>): BindToBuilder<Value> =>
396
- this.#createBindToBuilder(token, moduleRef),
367
+ this.#createBindToBuilder(token, registration),
397
368
  import: (...modules: Array<SyncModule | AsyncModule>): void => {
398
369
  for (const module of modules) {
399
370
  importPromises.push(this.#loadOneModuleAsync(module));
@@ -411,32 +382,33 @@ class DefaultContainer implements Container {
411
382
 
412
383
  /** Unregister module bindings and collect [binding, instance] pairs for deactivation. */
413
384
  #removeModuleBindings(ref: object): Array<[Binding, unknown]> {
414
- this.#moduleRefs.delete(ref);
415
- const ids = this.#moduleBindingIds.get(ref) ?? [];
416
- this.#moduleBindingIds.delete(ref);
385
+ this.#moduleRefs?.delete(ref);
386
+ const ids = this.#moduleBindingIds?.get(ref) ?? [];
387
+ this.#moduleBindingIds?.delete(ref);
417
388
  const pairs: Array<[Binding, unknown]> = [];
418
389
  for (const id of ids) {
419
390
  const binding = this.#registry.getById(id);
420
391
  if (binding !== undefined) {
421
392
  this.#registry.removeById(id);
422
- if (this.#scope.hasSingleton(id)) {
423
- pairs.push([binding, this.#scope.getSingleton(id)]);
424
- this.#scope.deleteSingleton(id);
393
+ if (binding.instance !== NO_INSTANCE) {
394
+ pairs.push([binding, binding.instance]);
395
+ this.#scope.deleteSingleton(binding);
425
396
  }
397
+ this.#scope.deleteScoped(binding.id);
426
398
  }
427
399
  }
428
400
  return pairs;
429
401
  }
430
402
 
431
403
  #unloadModuleSync(ref: object): void {
432
- const count = this.#moduleRefs.get(ref) ?? 0;
404
+ const count = this.#moduleRefs?.get(ref) ?? 0;
433
405
  if (count <= 1) {
434
406
  const reader = this.#getMetadataReader();
435
407
  for (const [binding, instance] of this.#removeModuleBindings(ref)) {
436
408
  this.#lifecycle.runDeactivationSync(binding, instance, reader);
437
409
  }
438
410
  } else {
439
- this.#moduleRefs.set(ref, count - 1);
411
+ this.#moduleRefs!.set(ref, count - 1);
440
412
  }
441
413
  }
442
414
 
@@ -448,14 +420,14 @@ class DefaultContainer implements Container {
448
420
  }
449
421
 
450
422
  async #unloadModuleAsync(ref: object): Promise<void> {
451
- const count = this.#moduleRefs.get(ref) ?? 0;
423
+ const count = this.#moduleRefs?.get(ref) ?? 0;
452
424
  if (count <= 1) {
453
425
  const reader = this.#getMetadataReader();
454
426
  for (const [binding, instance] of this.#removeModuleBindings(ref)) {
455
427
  await this.#lifecycle.runDeactivation(binding, instance, reader);
456
428
  }
457
429
  } else {
458
- this.#moduleRefs.set(ref, count - 1);
430
+ this.#moduleRefs!.set(ref, count - 1);
459
431
  }
460
432
  }
461
433
 
@@ -492,23 +464,33 @@ class DefaultContainer implements Container {
492
464
 
493
465
  resolve<const Value>(token: Token<Value> | Constructor<Value>, options?: ResolveOptions): Value {
494
466
  this.#assertNotDisposed();
467
+ const rootStack = this.#resolver.rootStack;
468
+ // A resolve already holding the shared pair means this one is nested; it mints its own.
469
+ if (rootStack.length !== 0) {
470
+ return options === undefined
471
+ ? this.#resolver.resolveFromContext(token, [], [])
472
+ : this.#resolver.resolve(token, options, [], []);
473
+ }
495
474
  if (options === undefined) {
496
- return this.#resolver.resolveFromContext(token, [], []);
475
+ return this.#resolver.resolveFromContext(token, this.#resolver.rootPath, rootStack);
497
476
  }
498
- return this.#resolver.resolve(token, options, [], []);
477
+ return this.#resolver.resolve(token, options, this.#resolver.rootPath, rootStack);
499
478
  }
500
479
 
501
480
  resolveAsync<const Value>(token: Token<Value> | Constructor<Value>, options?: ResolveOptions): Promise<Value> {
502
481
  this.#assertNotDisposed();
503
482
  if (options === undefined) {
504
- return this.#resolver.resolveAsyncFromContext(token, [], []);
483
+ return this.#resolver.resolveAsyncFromRoot(token) as Promise<Value>;
505
484
  }
506
- return this.#resolver.resolveAsync(token, options, [], []);
485
+ return this.#resolver.resolveAsync(token, options, [], [], ROOT_BRANCH);
507
486
  }
508
487
 
509
488
  resolveOptional<const Value>(token: Token<Value> | Constructor<Value>, options?: ResolveOptions): Value | undefined {
510
489
  this.#assertNotDisposed();
511
- return this.#resolver.resolveOptional(token, options, [], []);
490
+ const rootStack = this.#resolver.rootStack;
491
+ return rootStack.length === 0
492
+ ? this.#resolver.resolveOptional(token, options, this.#resolver.rootPath, rootStack)
493
+ : this.#resolver.resolveOptional(token, options, [], []);
512
494
  }
513
495
 
514
496
  resolveOptionalAsync<const Value>(
@@ -516,12 +498,15 @@ class DefaultContainer implements Container {
516
498
  options?: ResolveOptions,
517
499
  ): Promise<Value | undefined> {
518
500
  this.#assertNotDisposed();
519
- return this.#resolver.resolveOptionalAsync(token, options, [], []);
501
+ return this.#resolver.resolveOptionalAsync(token, options, [], [], ROOT_BRANCH);
520
502
  }
521
503
 
522
504
  resolveAll<const Value>(token: Token<Value> | Constructor<Value>, options?: ResolveOptions): Array<Value> {
523
505
  this.#assertNotDisposed();
524
- return this.#resolver.resolveAll(token, options, [], []);
506
+ const rootStack = this.#resolver.rootStack;
507
+ return rootStack.length === 0
508
+ ? this.#resolver.resolveAll(token, options, this.#resolver.rootPath, rootStack)
509
+ : this.#resolver.resolveAll(token, options, [], []);
525
510
  }
526
511
 
527
512
  resolveAllAsync<const Value>(
@@ -529,7 +514,7 @@ class DefaultContainer implements Container {
529
514
  options?: ResolveOptions,
530
515
  ): Promise<Array<Value>> {
531
516
  this.#assertNotDisposed();
532
- return this.#resolver.resolveAllAsync(token, options, [], []);
517
+ return this.#resolver.resolveAllAsync(token, options, [], [], ROOT_BRANCH);
533
518
  }
534
519
 
535
520
  // ── Child ─────────────────────────────────────────────────────────────────
@@ -549,11 +534,10 @@ class DefaultContainer implements Container {
549
534
 
550
535
  // Deactivate all singletons in this container (own only)
551
536
  const reader = this.#getMetadataReader();
552
- for (const [id, instance] of this.#scope.getAllSingletons()) {
553
- const binding = this.#registry.getById(id);
554
- if (binding !== undefined) {
555
- await this.#lifecycle.runDeactivation(binding, instance, reader);
556
- }
537
+ // Iterate a copy: a deactivation handler is user code, and the live list is what
538
+ // materializing or dropping a singleton mutates.
539
+ for (const binding of this.#scope.cachedSingletons().slice()) {
540
+ await this.#lifecycle.runDeactivation(binding, binding.instance, reader);
557
541
  }
558
542
 
559
543
  this.#scope.clearAll();
@@ -577,7 +561,7 @@ class DefaultContainer implements Container {
577
561
  continue;
578
562
  }
579
563
  const scope = effectiveBindingScope(binding);
580
- if (scope === "singleton" && !this.#scope.hasSingleton(binding.id)) {
564
+ if (scope === "singleton" && binding.instance === NO_INSTANCE) {
581
565
  if (binding.predicate !== undefined) {
582
566
  continue;
583
567
  }
@@ -633,10 +617,7 @@ class DefaultContainer implements Container {
633
617
  for (const edge of this.#collectStaticDependencyEdges(current, reader)) {
634
618
  const { terminal, depTokenName } = edge;
635
619
  const depScope = this.#validationScopeFromTerminal(terminal);
636
- if (depScope === "opaque") {
637
- continue;
638
- }
639
- if (depScope === "scoped" || depScope === "transient") {
620
+ if (depScope !== "singleton") {
640
621
  throw new ScopeViolationError({
641
622
  consumerToken: rootName,
642
623
  consumerScope: "singleton",
@@ -654,27 +635,18 @@ class DefaultContainer implements Container {
654
635
  dfs(root, [rootName], new Set());
655
636
  }
656
637
 
657
- #validationScopeFromTerminal(terminal: Binding): BindingScope | "opaque" {
658
- switch (terminal.kind) {
659
- case "constant":
660
- return "singleton";
661
- // A factory's *body* is not statically analyzable, but the scope it was bound with is
662
- // declared like any other — so the captive-dependency check applies. The DFS below still
663
- // refuses to descend into the factory; only this edge is judged.
664
- case "dynamic":
665
- case "dynamic-async":
666
- return terminal.scope;
667
- case "class":
668
- case "resolved":
669
- case "resolved-async":
670
- return terminal.scope;
671
- case "alias":
672
- throw new InternalError("validate: expected terminal binding after alias resolution");
673
- default: {
674
- const exhaustive: never = terminal;
675
- return exhaustive;
676
- }
638
+ /**
639
+ * The scope this edge is judged against.
640
+ *
641
+ * @remarks A factory's *body* is not statically analyzable, but the scope it was bound with is
642
+ * declared like any other — so the captive-dependency check applies to it too. The DFS still
643
+ * refuses to descend into a factory; only the edge is judged.
644
+ */
645
+ #validationScopeFromTerminal(terminal: Binding): BindingScope {
646
+ if (terminal.kind === "alias") {
647
+ throw new InternalError("validate: expected terminal binding after alias resolution");
677
648
  }
649
+ return terminal.scope;
678
650
  }
679
651
 
680
652
  #followAliasChainToTerminal(binding: Binding, options: ResolveOptions | undefined): Binding | undefined {
@@ -698,75 +670,43 @@ class DefaultContainer implements Container {
698
670
  return current;
699
671
  }
700
672
 
673
+ /** What one dependency could resolve to: every candidate for `injectAll`, else at most one. */
674
+ #peekDependencyCandidates(dep: DependencySlot, options: ResolveOptions | undefined): ReadonlyArray<Binding> {
675
+ if (dep.multi) {
676
+ return this.#resolver.peekCandidateBindingsForValidate(dep.token, options);
677
+ }
678
+ const found = this.#resolver.peekBindingForValidate(dep.token, options);
679
+ return found === undefined ? [] : [found.binding];
680
+ }
681
+
682
+ /** What a binding declares up front — a class's params, a factory's descriptors, else nothing. */
683
+ #staticDependencies(binding: Binding, reader: MetadataReader): ReadonlyArray<DependencySlot> {
684
+ if (binding.kind === "class") {
685
+ return reader.getConstructorMetadata(binding.target as Constructor)?.params ?? [];
686
+ }
687
+ if (binding.kind === "resolved" || binding.kind === "resolved-async") {
688
+ return binding.deps;
689
+ }
690
+ return [];
691
+ }
692
+
701
693
  #collectStaticDependencyEdges(
702
694
  binding: Binding,
703
695
  reader: MetadataReader,
704
696
  ): Array<{ terminal: Binding; depTokenName: string }> {
705
697
  const edges: Array<{ terminal: Binding; depTokenName: string }> = [];
706
698
 
707
- const pushTerminal = (terminal: Binding | undefined, displayName: string): void => {
708
- if (terminal === undefined) {
709
- return;
710
- }
711
- edges.push({ terminal, depTokenName: displayName });
712
- };
713
-
714
- if (binding.kind === "class") {
715
- const meta = reader.getConstructorMetadata(binding.target as Constructor);
716
- if (meta === undefined) {
717
- return edges;
718
- }
719
- for (const param of meta.params) {
720
- const paramOptions = injectionSlotToResolveOptions(param);
721
- if (param.optional) {
722
- continue;
723
- }
724
- const tokenRef = param.token;
725
- if (param.multi) {
726
- const candidates = this.#resolver.peekCandidateBindingsForValidate(tokenRef, paramOptions);
727
- for (const cand of candidates) {
728
- const term = this.#followAliasChainToTerminal(cand, paramOptions);
729
- pushTerminal(term, term !== undefined ? tokenName(term.token as Token<unknown>) : "");
730
- }
731
- continue;
732
- }
733
- const found =
734
- paramOptions === undefined
735
- ? this.#resolver.peekBindingForValidate(tokenRef, undefined)
736
- : this.#resolver.peekBindingForValidate(tokenRef, paramOptions);
737
- if (found === undefined) {
738
- continue;
739
- }
740
- const term = this.#followAliasChainToTerminal(found.binding, paramOptions);
741
- pushTerminal(term, term !== undefined ? tokenName(term.token as Token<unknown>) : "");
699
+ for (const dep of this.#staticDependencies(binding, reader)) {
700
+ // An optional dependency imposes no scope constraint: it may legitimately be absent.
701
+ if (dep.optional) {
702
+ continue;
742
703
  }
743
- return edges;
744
- }
745
-
746
- if (binding.kind === "resolved" || binding.kind === "resolved-async") {
747
- for (const dep of binding.deps) {
748
- const depOptions = injectionSlotToResolveOptions(dep);
749
- if (dep.optional) {
750
- continue;
751
- }
752
- const tokenRef = dep.token as Token<unknown> | Constructor;
753
- if (dep.multi) {
754
- const candidates = this.#resolver.peekCandidateBindingsForValidate(tokenRef, depOptions);
755
- for (const cand of candidates) {
756
- const term = this.#followAliasChainToTerminal(cand, depOptions);
757
- pushTerminal(term, term !== undefined ? tokenName(term.token as Token<unknown>) : "");
758
- }
759
- continue;
760
- }
761
- const found =
762
- depOptions === undefined
763
- ? this.#resolver.peekBindingForValidate(tokenRef, undefined)
764
- : this.#resolver.peekBindingForValidate(tokenRef, depOptions);
765
- if (found === undefined) {
766
- continue;
704
+ const depOptions = injectionSlotToResolveOptions(dep);
705
+ for (const candidate of this.#peekDependencyCandidates(dep, depOptions)) {
706
+ const terminal = this.#followAliasChainToTerminal(candidate, depOptions);
707
+ if (terminal !== undefined) {
708
+ edges.push({ terminal, depTokenName: tokenName(terminal.token as Token<unknown>) });
767
709
  }
768
- const term = this.#followAliasChainToTerminal(found.binding, depOptions);
769
- pushTerminal(term, term !== undefined ? tokenName(term.token as Token<unknown>) : "");
770
710
  }
771
711
  }
772
712
 
@@ -777,22 +717,22 @@ class DefaultContainer implements Container {
777
717
 
778
718
  has(token: Token<unknown> | Constructor, options?: ResolveOptions): boolean {
779
719
  this.#assertNotDisposed();
780
- return this.#inspector.has(token, options, () => this.#parent?.has(token, options) ?? false);
720
+ return this.#getInspector().has(token, options, () => this.#parent?.has(token, options) ?? false);
781
721
  }
782
722
 
783
723
  hasOwn(token: Token<unknown> | Constructor, options?: ResolveOptions): boolean {
784
724
  this.#assertNotDisposed();
785
- return this.#inspector.hasOwn(token, options);
725
+ return this.#getInspector().hasOwn(token, options);
786
726
  }
787
727
 
788
728
  lookupBindings<const Value>(token: Token<Value> | Constructor<Value>): ReadonlyArray<BindingSnapshot> {
789
729
  this.#assertNotDisposed();
790
- return this.#inspector.lookupBindings(token);
730
+ return this.#getInspector().lookupBindings(token);
791
731
  }
792
732
 
793
733
  inspect(): ContainerSnapshot {
794
734
  this.#assertNotDisposed();
795
- return this.#inspector.inspect();
735
+ return this.#getInspector().inspect();
796
736
  }
797
737
 
798
738
  generateDependencyGraph(options?: GraphOptions): ContainerGraphJson {
@@ -34,10 +34,8 @@ export type InjectableDependency<Value = unknown> = Token<Value> | Constructor<V
34
34
  /**
35
35
  * The value a factory receives for one declared dependency.
36
36
  *
37
- * @remarks `optional()` and `injectAll()` already fold their effect into the descriptor's own
38
- * type parameter — `InjectionDescriptor<Value | undefined>` and `InjectionDescriptor<Array<Value>>`
39
- * — so reading that parameter back is enough; bare tokens and constructors fall through to
40
- * {@link TokenValue}.
37
+ * @remarks `optional()` and `injectAll()` fold their effect into the descriptor's own type
38
+ * parameter, so reading it back is enough; bare tokens fall through to {@link TokenValue}.
41
39
  *
42
40
  * @since 0.5.0-canary.7
43
41
  */
@@ -195,7 +193,7 @@ export function inject<const Value>(
195
193
  }
196
194
  Object.defineProperties(decoratorFn, props);
197
195
 
198
- return decoratorFn as unknown as InjectionDescriptor<Value> & ClassAccessorDecorator<unknown, Value>;
196
+ return decoratorFn as InjectionDescriptor<Value> & ClassAccessorDecorator<unknown, Value>;
199
197
  }
200
198
 
201
199
  // ── optional() ────────────────────────────────────────────────────────────────