@codefast/di 0.3.16-canary.2 → 0.3.16-canary.3

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 (60) hide show
  1. package/CHANGELOG.md +8 -0
  2. package/dist/binding.d.mts +24 -24
  3. package/dist/constraints.d.mts +3 -3
  4. package/dist/container.mjs +82 -150
  5. package/dist/decorators/inject.d.mts +3 -2
  6. package/dist/decorators/inject.mjs +13 -40
  7. package/dist/decorators/injectable.mjs +4 -12
  8. package/dist/decorators/lifecycle-decorators.mjs +11 -72
  9. package/dist/dependency-graph.mjs +6 -4
  10. package/dist/graph-adapters/cytoscape.d.mts +12 -12
  11. package/dist/graph-adapters/cytoscape.mjs +7 -7
  12. package/dist/graph-adapters/reactflow.d.mts +15 -15
  13. package/dist/graph-adapters/reactflow.mjs +6 -9
  14. package/dist/index.d.mts +3 -3
  15. package/dist/inspector.d.mts +3 -2
  16. package/dist/inspector.mjs +7 -10
  17. package/dist/lifecycle.mjs +2 -12
  18. package/dist/metadata/metadata-keys.d.mts +8 -33
  19. package/dist/metadata/metadata-keys.mjs +8 -21
  20. package/dist/metadata/metadata-types.d.mts +5 -0
  21. package/dist/metadata/symbol-metadata-reader.d.mts +1 -0
  22. package/dist/metadata/symbol-metadata-reader.mjs +11 -33
  23. package/dist/registry.mjs +3 -22
  24. package/dist/resolve-options.d.mts +2 -2
  25. package/dist/resolve-options.mjs +10 -8
  26. package/dist/resolver.d.mts +5 -0
  27. package/dist/resolver.mjs +15 -21
  28. package/dist/types.d.mts +10 -4
  29. package/package.json +39 -13
  30. package/src/binding-scope.ts +26 -0
  31. package/src/binding-select.ts +167 -0
  32. package/src/binding.ts +281 -0
  33. package/src/constraints.ts +149 -0
  34. package/src/constructor-type.ts +19 -0
  35. package/src/container.ts +1213 -0
  36. package/src/decorators/inject.ts +233 -0
  37. package/src/decorators/injectable.ts +85 -0
  38. package/src/decorators/lifecycle-decorators.ts +55 -0
  39. package/src/dependency-graph.ts +116 -0
  40. package/src/environment.ts +232 -0
  41. package/src/errors.ts +262 -0
  42. package/src/graph-adapters/cytoscape.ts +64 -0
  43. package/src/graph-adapters/dot.ts +22 -0
  44. package/src/graph-adapters/reactflow.ts +58 -0
  45. package/src/index.ts +101 -0
  46. package/src/inspector.ts +133 -0
  47. package/src/lifecycle.ts +238 -0
  48. package/src/metadata/metadata-keys.ts +25 -0
  49. package/src/metadata/metadata-reader-token.ts +8 -0
  50. package/src/metadata/metadata-types.ts +53 -0
  51. package/src/metadata/symbol-metadata-reader.ts +57 -0
  52. package/src/module.ts +93 -0
  53. package/src/registry.ts +241 -0
  54. package/src/resolve-options.ts +42 -0
  55. package/src/resolver.ts +1837 -0
  56. package/src/scope.ts +77 -0
  57. package/src/token.ts +40 -0
  58. package/src/types.ts +145 -0
  59. package/dist/graph-adapters/types.d.mts +0 -2
  60. package/dist/graph-adapters/types.mjs +0 -1
@@ -0,0 +1,1837 @@
1
+ import type { ConstructorInvocation } from "#/constructor-type";
2
+ import type {
3
+ BindingIdentifier,
4
+ BindingScope,
5
+ BindingTag,
6
+ ConstraintContext,
7
+ Constructor,
8
+ ResolutionFrame,
9
+ ResolveOptions,
10
+ } from "#/types";
11
+ import type { Token } from "#/token";
12
+ import type { Binding, BindingSlot } from "#/binding";
13
+ import type { BindingRegistry } from "#/registry";
14
+ import type { ScopeManager } from "#/scope";
15
+ import type { LifecycleManager } from "#/lifecycle";
16
+ import type { ConstructorMetadata, MetadataReader } from "#/metadata/metadata-types";
17
+ import type { InjectionDescriptor } from "#/decorators/inject";
18
+ import type { Container } from "#/container";
19
+ import {
20
+ AsyncResolutionError,
21
+ CircularDependencyError,
22
+ InternalError,
23
+ MissingMetadataError,
24
+ MissingScopeContextError,
25
+ NoMatchingBindingError,
26
+ TokenNotBoundError,
27
+ } from "#/errors";
28
+ import { tokenName } from "#/token";
29
+ import type { ResolverCallbacks } from "#/environment";
30
+ import { buildResolutionFrame, DefaultResolutionContext, runWithContainer } from "#/environment";
31
+ import { injectionSlotToResolveOptions } from "#/resolve-options";
32
+ import { selectAllBindings, selectBinding } from "#/binding-select";
33
+
34
+ type BindingWithScope = Binding & { scope: BindingScope };
35
+ const RESOLUTION_SET_KEY: unique symbol = Symbol("di:resolution-set");
36
+ const RESOLUTION_SET_THRESHOLD = 32;
37
+ type ResolutionPathWithSet = Array<string> & { [RESOLUTION_SET_KEY]?: Set<string> };
38
+ const EMPTY_STRING_LIST: ReadonlyArray<string> = [];
39
+ const EMPTY_FRAME_LIST: ReadonlyArray<ResolutionFrame> = [];
40
+ const ROOT_CONSTRAINT_CONTEXT = {
41
+ resolutionPath: EMPTY_STRING_LIST,
42
+ resolutionStack: EMPTY_FRAME_LIST,
43
+ parent: undefined,
44
+ ancestors: EMPTY_FRAME_LIST,
45
+ currentResolveHint: undefined,
46
+ };
47
+
48
+ /**
49
+ * @since 0.3.16-canary.0
50
+ */
51
+ export class DependencyResolver {
52
+ private readonly _frameByBindingId = new Map<BindingIdentifier, ResolutionFrame>();
53
+ private readonly _syncResolutionContextPool: Array<DefaultResolutionContext> = [];
54
+ // Deep-path state for _resolveTransientDynamicSyncFromContext (depth ≥ RESOLUTION_SET_THRESHOLD):
55
+ //
56
+ // Cycle detection — generation-based marking (replaces Set<BindingIdentifier>):
57
+ // _deepCycleMarks: Map<BindingIdentifier, generation> records which bindings are "in flight"
58
+ // during the current deep chain. A binding is considered "active" when its recorded
59
+ // generation matches _deepCycleGen.
60
+ // _deepCycleGen: monotonically incremented at the start of each new deep chain. This acts
61
+ // as an implicit bulk-clear: old marks from a previous chain have a stale generation number
62
+ // and are treated as absent — no explicit Map.delete or Map.clear is needed at all.
63
+ // Eliminating the per-level Map.delete call (was ~10 ns × 480 levels) is the primary
64
+ // motivation for this design.
65
+ //
66
+ // _deepActiveLevels: tracks how many deep levels are currently on the call stack so we know
67
+ // when the deep portion has fully unwound and can reset _deepSyncCtxPath.
68
+ //
69
+ // _deepSyncCtx / _deepSyncCtxPath: single shared context for the deep chain; avoids one
70
+ // per-depth pool reset (5 property writes) for every level beyond the threshold.
71
+ // resolutionStack is NOT pushed in the deep path — no GC write-barriers for object arrays.
72
+ // Trade-off: ctx.graph.resolutionStack only reflects the first RESOLUTION_SET_THRESHOLD
73
+ // frames; code relying on ctx.graph.parent inside a deep transient-dynamic factory will see
74
+ // the frame at the threshold boundary, not the current depth.
75
+ private readonly _deepCycleMarks = new Map<BindingIdentifier, number>();
76
+ private _deepCycleGen = 0;
77
+ private _deepActiveLevels = 0;
78
+ private _deepSyncCtx: DefaultResolutionContext | undefined;
79
+ private _deepSyncCtxPath: Array<string> | undefined;
80
+ // Async shared-context state for _resolveTransientDynamicAsyncFromContext (shallow path):
81
+ //
82
+ // For a SEQUENTIAL async chain (depth < RESOLUTION_SET_THRESHOLD), all levels share the same
83
+ // resolutionPath and resolutionStack arrays (passed by reference through ctx.resolveAsync).
84
+ // Because the context stores references rather than snapshots, a single DefaultResolutionContext
85
+ // can serve the entire chain without any per-level allocation or reset — the arrays reflect the
86
+ // current chain state automatically as we push/pop.
87
+ //
88
+ // _deepAsyncCtx: the shared context, lazily created on the first chain entry and reset
89
+ // (5 property writes) at the start of each new root call. Levels 2-N of the same chain
90
+ // reuse it with ZERO setup cost.
91
+ //
92
+ // _deepAsyncCtxPath: identity pointer of the resolutionPath array that "owns" the shared
93
+ // context. Used to distinguish two cases:
94
+ // • same reference → inner level of the owning chain → reuse ctx with no setup
95
+ // • different reference → concurrent chain (e.g. Promise.all) → fall back to a fresh
96
+ // DefaultResolutionContext allocation for that call
97
+ //
98
+ // _deepAsyncActiveLevels: counts active levels of the OWNING chain so we know when to
99
+ // release the path pointer (set _deepAsyncCtxPath = undefined).
100
+ // Concurrent fallback calls are NOT counted — they don't interfere with the owner.
101
+ //
102
+ // resolutionStack is NOT pushed in this path (same trade-off as the deep sync path):
103
+ // ctx.graph.resolutionStack will always be empty for async transient-dynamic chains.
104
+ // Factories that inspect ctx.graph.parent should use the conventional sync binding approach.
105
+ //
106
+ // The function is NOT declared async so that V8 does not create an AsyncGeneratorObject and
107
+ // an implicit Promise on every invocation; instead each level calls factoryPromise.then(cleanup)
108
+ // which chains natively without the extra async state-machine overhead.
109
+ private _deepAsyncCtx: DefaultResolutionContext | undefined;
110
+ private _deepAsyncCtxPath: Array<string> | undefined;
111
+ private _deepAsyncActiveLevels = 0;
112
+ private readonly _classHasPostConstruct = new WeakMap<Constructor, boolean>();
113
+ private readonly _classNeedsActiveContainer = new WeakMap<Constructor, boolean>();
114
+ private readonly _classConstructorMetadata = new WeakMap<
115
+ Constructor,
116
+ ConstructorMetadata | null
117
+ >();
118
+ private readonly _activationNeedByBindingId = new Map<BindingIdentifier, boolean>();
119
+ private _activationCacheVersion = -1;
120
+
121
+ constructor(
122
+ private readonly _registry: BindingRegistry,
123
+ private readonly _scope: ScopeManager,
124
+ private readonly _lifecycle: LifecycleManager,
125
+ private readonly _metadataReader: MetadataReader,
126
+ private readonly _container: Container,
127
+ private readonly _parent: DependencyResolver | undefined,
128
+ ) {}
129
+
130
+ // ── Binding lookup ─────────────────────────────────────────────────────────
131
+
132
+ private _findBinding(
133
+ token: Token<unknown> | Constructor,
134
+ hint: ResolveOptions | undefined,
135
+ resolutionPath: Array<string>,
136
+ resolutionStack: Array<ResolutionFrame>,
137
+ ): { binding: Binding; owner: DependencyResolver } | undefined {
138
+ if (hint === undefined) {
139
+ const fastDefaultBinding = this._registry.getFastDefault(token);
140
+ if (fastDefaultBinding !== undefined) {
141
+ return { binding: fastDefaultBinding, owner: this };
142
+ }
143
+ }
144
+
145
+ if (hint?.name !== undefined && hint.tag === undefined && (hint.tags?.length ?? 0) === 0) {
146
+ const namedBinding = this._registry.getSimpleNamed(token, hint.name);
147
+ if (
148
+ namedBinding !== undefined &&
149
+ this._matchesBindingFast(namedBinding, hint, resolutionPath, resolutionStack)
150
+ ) {
151
+ return { binding: namedBinding, owner: this };
152
+ }
153
+ }
154
+
155
+ if (
156
+ hint !== undefined &&
157
+ hint.name === undefined &&
158
+ hint.tag === undefined &&
159
+ (hint.tags?.length ?? 0) === 1
160
+ ) {
161
+ const [tagKey, tagValue] = hint.tags![0]!;
162
+ const tagged = this._registry.getSimpleTagged(token, tagKey, tagValue);
163
+ if (tagged !== undefined) {
164
+ return { binding: tagged, owner: this };
165
+ }
166
+ }
167
+
168
+ const bindings = this._registry.getAll(token);
169
+ if (bindings.length > 0) {
170
+ if (bindings.length === 1) {
171
+ const onlyBinding = bindings[0]!;
172
+ const isDefaultSlot =
173
+ onlyBinding.slot.name === undefined && onlyBinding.slot.tags.length === 0;
174
+ if (hint === undefined && isDefaultSlot && onlyBinding.predicate === undefined) {
175
+ return { binding: onlyBinding, owner: this };
176
+ }
177
+ if (this._matchesBindingFast(onlyBinding, hint, resolutionPath, resolutionStack)) {
178
+ return { binding: onlyBinding, owner: this };
179
+ }
180
+ }
181
+ const ctx = this._makeConstraintContext(resolutionPath, resolutionStack, hint);
182
+ const binding = selectBinding(bindings, hint, ctx, this._getTokenName(token));
183
+ if (binding !== undefined) {
184
+ return { binding, owner: this };
185
+ }
186
+ }
187
+ if (this._parent !== undefined) {
188
+ return this._parent._findBinding(token, hint, resolutionPath, resolutionStack);
189
+ }
190
+ return undefined;
191
+ }
192
+
193
+ /**
194
+ * Binding lookup aligned with `resolve` — used by `Container.validate` without instantiating.
195
+ */
196
+ peekBindingForValidate(
197
+ token: Token<unknown> | Constructor,
198
+ hint: ResolveOptions | undefined,
199
+ ): { binding: Binding; owner: DependencyResolver } | undefined {
200
+ return this._findBinding(token, hint, [], []);
201
+ }
202
+
203
+ /**
204
+ * Mirrors {@link DependencyResolver.resolveAll} candidate selection only (no instantiation).
205
+ */
206
+ peekCandidateBindingsForValidate(
207
+ token: Token<unknown> | Constructor,
208
+ hint: ResolveOptions | undefined,
209
+ ): Array<Binding> {
210
+ if (hint?.name !== undefined && hint.tag === undefined && (hint.tags?.length ?? 0) === 0) {
211
+ return this._getSimpleNamedBindingsFromChain(token, hint.name);
212
+ }
213
+ const allBindings = this._getAllBindingsFromChain(token);
214
+ if (allBindings.length === 0) {
215
+ return [];
216
+ }
217
+ const ctx = this._makeConstraintContext([], [], hint);
218
+ return selectAllBindings(allBindings, hint, ctx);
219
+ }
220
+
221
+ // ── Sync resolve ───────────────────────────────────────────────────────────
222
+
223
+ resolveFromContext<const Value>(
224
+ token: Token<Value> | Constructor<Value>,
225
+ resolutionPath: Array<string>,
226
+ resolutionStack: Array<ResolutionFrame>,
227
+ ): Value {
228
+ const fastBinding = this._registry.getFastDefault(token);
229
+ if (fastBinding !== undefined) {
230
+ if (fastBinding.kind === "alias") {
231
+ return this.resolveFromContext(
232
+ fastBinding.target as Token<Value> | Constructor<Value>,
233
+ resolutionPath,
234
+ resolutionStack,
235
+ );
236
+ }
237
+ const scope = (fastBinding as BindingWithScope).scope ?? "transient";
238
+ if (
239
+ scope === "transient" &&
240
+ fastBinding.kind === "dynamic" &&
241
+ fastBinding.onActivation === undefined &&
242
+ (this._lifecycle.activationVersion === 0 ||
243
+ !this._lifecycle.hasActivationHandlers(fastBinding.token))
244
+ ) {
245
+ return this._resolveTransientDynamicSyncFromContext(
246
+ fastBinding as Binding<Value> & { kind: "dynamic" },
247
+ resolutionPath,
248
+ resolutionStack,
249
+ );
250
+ }
251
+ if (scope === "singleton" && this._scope.hasSingleton(fastBinding.id)) {
252
+ return this._scope.getSingleton<Value>(fastBinding.id);
253
+ }
254
+ if (scope === "scoped") {
255
+ if (!this._scope.isChild) {
256
+ throw new MissingScopeContextError(this._getTokenName(fastBinding.token));
257
+ }
258
+ if (this._scope.hasScoped(fastBinding.id)) {
259
+ return this._scope.getScoped<Value>(fastBinding.id);
260
+ }
261
+ }
262
+ return this._resolveBinding(
263
+ fastBinding as Binding<Value>,
264
+ undefined,
265
+ resolutionPath,
266
+ resolutionStack,
267
+ );
268
+ }
269
+
270
+ return this.resolve(token, undefined, resolutionPath, resolutionStack);
271
+ }
272
+
273
+ resolve<const Value>(
274
+ token: Token<Value> | Constructor<Value>,
275
+ hint: ResolveOptions | undefined,
276
+ resolutionPath: Array<string>,
277
+ resolutionStack: Array<ResolutionFrame>,
278
+ ): Value {
279
+ const found = this._findBinding(token, hint, resolutionPath, resolutionStack);
280
+
281
+ if (found === undefined) {
282
+ const ownBindings = this._registry.getAll(token);
283
+ if (ownBindings.length > 0) {
284
+ throw new NoMatchingBindingError(
285
+ this._getTokenName(token),
286
+ hint ?? {},
287
+ this._getAvailableSlots(token),
288
+ );
289
+ }
290
+ throw new TokenNotBoundError(this._getTokenName(token));
291
+ }
292
+
293
+ const { binding, owner } = found;
294
+
295
+ // Follow alias
296
+ if (binding.kind === "alias") {
297
+ return this.resolve(
298
+ binding.target as Token<Value> | Constructor<Value>,
299
+ hint,
300
+ resolutionPath,
301
+ resolutionStack,
302
+ );
303
+ }
304
+
305
+ const scope = (binding as BindingWithScope).scope ?? "transient";
306
+
307
+ // Singleton from a parent resolver: delegate so the parent caches it correctly
308
+ if (scope === "singleton" && owner !== this) {
309
+ return owner._resolveBinding(
310
+ binding as Binding<Value>,
311
+ hint,
312
+ resolutionPath,
313
+ resolutionStack,
314
+ );
315
+ }
316
+
317
+ // Scoped/transient (or own singleton): resolve with this resolver's container/scope
318
+ return this._resolveBinding(binding as Binding<Value>, hint, resolutionPath, resolutionStack);
319
+ }
320
+
321
+ private _resolveBinding<const Value>(
322
+ binding: Binding<Value>,
323
+ hint: ResolveOptions | undefined,
324
+ resolutionPath: Array<string>,
325
+ resolutionStack: Array<ResolutionFrame>,
326
+ ): Value {
327
+ if (
328
+ binding.kind === "constant" &&
329
+ binding.onActivation === undefined &&
330
+ !this._lifecycle.hasActivationHandlers(binding.token)
331
+ ) {
332
+ return binding.value;
333
+ }
334
+
335
+ const scope = (binding as BindingWithScope).scope ?? "transient";
336
+
337
+ // Singleton cache check
338
+ if (scope === "singleton") {
339
+ if (this._scope.hasSingleton(binding.id)) {
340
+ return this._scope.getSingleton<Value>(binding.id);
341
+ }
342
+ }
343
+
344
+ // Scoped cache check
345
+ if (scope === "scoped") {
346
+ if (!this._scope.isChild) {
347
+ throw new MissingScopeContextError(this._getTokenName(binding.token));
348
+ }
349
+ if (this._scope.hasScoped(binding.id)) {
350
+ return this._scope.getScoped<Value>(binding.id);
351
+ }
352
+ }
353
+
354
+ const frame = this._getResolutionFrame(binding);
355
+ const tokenDisplayName = frame.tokenName;
356
+ const pathWithSet = resolutionPath as ResolutionPathWithSet;
357
+ let resolutionSet = pathWithSet[RESOLUTION_SET_KEY];
358
+ if (resolutionSet === undefined && resolutionPath.length >= RESOLUTION_SET_THRESHOLD) {
359
+ resolutionSet = new Set<string>(resolutionPath);
360
+ pathWithSet[RESOLUTION_SET_KEY] = resolutionSet;
361
+ }
362
+
363
+ // Circular dependency detection
364
+ if (
365
+ resolutionSet !== undefined
366
+ ? resolutionSet.has(tokenDisplayName)
367
+ : resolutionPath.includes(tokenDisplayName)
368
+ ) {
369
+ const cycle = [...resolutionPath, tokenDisplayName];
370
+ throw new CircularDependencyError(cycle);
371
+ }
372
+
373
+ resolutionPath.push(tokenDisplayName);
374
+ resolutionSet?.add(tokenDisplayName);
375
+ resolutionStack.push(frame);
376
+ const needsActivation = this._needsActivation(binding);
377
+ if (!needsActivation && scope === "transient" && binding.kind === "dynamic") {
378
+ const resolutionCtx = this._acquireSyncResolutionContext(
379
+ resolutionPath,
380
+ resolutionStack,
381
+ hint,
382
+ );
383
+ try {
384
+ const dynamicResult = binding.factory(resolutionCtx);
385
+ if (dynamicResult instanceof Promise) {
386
+ throw new AsyncResolutionError(tokenName(binding.token), tokenName(binding.token));
387
+ }
388
+ resolutionStack.pop();
389
+ resolutionPath.pop();
390
+ resolutionSet?.delete(tokenDisplayName);
391
+ return dynamicResult;
392
+ } catch (error) {
393
+ resolutionStack.pop();
394
+ resolutionPath.pop();
395
+ resolutionSet?.delete(tokenDisplayName);
396
+ throw error;
397
+ }
398
+ }
399
+
400
+ try {
401
+ const needsResolutionContext = needsActivation || this._requiresResolutionContext(binding);
402
+ const resolutionCtx = needsResolutionContext
403
+ ? this._acquireSyncResolutionContext(resolutionPath, resolutionStack, hint)
404
+ : undefined;
405
+
406
+ const instance = this._instantiateSync(
407
+ binding,
408
+ resolutionCtx,
409
+ resolutionPath,
410
+ resolutionStack,
411
+ );
412
+
413
+ const shouldActivate = this._refreshActivationCacheIfNeeded(binding, needsActivation);
414
+ const activated = shouldActivate
415
+ ? this._lifecycle.runActivationSync(
416
+ resolutionCtx as DefaultResolutionContext,
417
+ binding,
418
+ instance,
419
+ this._metadataReader,
420
+ )
421
+ : instance;
422
+
423
+ // Cache by scope
424
+ if (scope === "singleton") {
425
+ this._scope.setSingleton(binding.id, activated);
426
+ } else if (scope === "scoped") {
427
+ this._scope.setScoped(binding.id, activated);
428
+ }
429
+
430
+ return activated;
431
+ } finally {
432
+ resolutionStack.pop();
433
+ resolutionPath.pop();
434
+ resolutionSet?.delete(tokenDisplayName);
435
+ }
436
+ }
437
+
438
+ private _instantiateSync<const Value>(
439
+ binding: Binding<Value>,
440
+ ctx: DefaultResolutionContext | undefined,
441
+ resolutionPath: Array<string>,
442
+ resolutionStack: Array<ResolutionFrame>,
443
+ ): Value {
444
+ switch (binding.kind) {
445
+ case "constant":
446
+ return binding.value;
447
+
448
+ case "dynamic": {
449
+ if (ctx === undefined) {
450
+ throw new InternalError("dynamic binding requires resolution context");
451
+ }
452
+ const factoryResult = binding.factory(ctx);
453
+ if (factoryResult instanceof Promise) {
454
+ throw new AsyncResolutionError(tokenName(binding.token), tokenName(binding.token));
455
+ }
456
+ return factoryResult;
457
+ }
458
+
459
+ case "dynamic-async":
460
+ throw new AsyncResolutionError(tokenName(binding.token), tokenName(binding.token));
461
+
462
+ case "class": {
463
+ const deps = this._resolveClassDeps(binding.target, resolutionPath, resolutionStack);
464
+ const instance = this._instantiateClass(binding.target, deps);
465
+ return instance as Value;
466
+ }
467
+
468
+ case "resolved": {
469
+ const deps = this._resolveDescriptorDeps(binding.deps, resolutionPath, resolutionStack);
470
+ const factoryResult = binding.factory(...deps);
471
+ if (factoryResult instanceof Promise) {
472
+ throw new AsyncResolutionError(tokenName(binding.token), tokenName(binding.token));
473
+ }
474
+ return factoryResult;
475
+ }
476
+
477
+ case "resolved-async":
478
+ throw new AsyncResolutionError(tokenName(binding.token), tokenName(binding.token));
479
+
480
+ case "alias":
481
+ throw new InternalError("alias should have been followed before instantiation");
482
+ }
483
+ }
484
+
485
+ private _resolveClassDeps(
486
+ target: Constructor,
487
+ resolutionPath: Array<string>,
488
+ resolutionStack: Array<ResolutionFrame>,
489
+ ): Array<unknown> {
490
+ const meta = this._getConstructorMetadata(target);
491
+ if (meta === undefined) {
492
+ if (target.length === 0) {
493
+ return [];
494
+ }
495
+ throw new MissingMetadataError(target.name);
496
+ }
497
+ if (meta.params.length === 0) {
498
+ return [];
499
+ }
500
+ if (meta.params.length === 1) {
501
+ const param = meta.params[0]!;
502
+ const paramHint = injectionSlotToResolveOptions(param);
503
+ if (param.multi) {
504
+ return [
505
+ this.resolveAll(
506
+ param.token as Token<unknown> | Constructor,
507
+ paramHint,
508
+ resolutionPath,
509
+ resolutionStack,
510
+ ),
511
+ ];
512
+ }
513
+ if (param.optional) {
514
+ return [
515
+ this.resolveOptional(
516
+ param.token as Token<unknown> | Constructor,
517
+ paramHint,
518
+ resolutionPath,
519
+ resolutionStack,
520
+ ),
521
+ ];
522
+ }
523
+ if (paramHint === undefined) {
524
+ return [
525
+ this.resolveFromContext(
526
+ param.token as Token<unknown> | Constructor,
527
+ resolutionPath,
528
+ resolutionStack,
529
+ ),
530
+ ];
531
+ }
532
+ return [
533
+ this.resolve(
534
+ param.token as Token<unknown> | Constructor,
535
+ paramHint,
536
+ resolutionPath,
537
+ resolutionStack,
538
+ ),
539
+ ];
540
+ }
541
+ const deps = new Array<unknown>(meta.params.length);
542
+ for (let index = 0; index < meta.params.length; index += 1) {
543
+ const param = meta.params[index]!;
544
+ const paramHint = injectionSlotToResolveOptions(param);
545
+ if (param.multi) {
546
+ deps[index] = this.resolveAll(
547
+ param.token as Token<unknown> | Constructor,
548
+ paramHint,
549
+ resolutionPath,
550
+ resolutionStack,
551
+ );
552
+ continue;
553
+ }
554
+ if (param.optional) {
555
+ deps[index] = this.resolveOptional(
556
+ param.token as Token<unknown> | Constructor,
557
+ paramHint,
558
+ resolutionPath,
559
+ resolutionStack,
560
+ );
561
+ continue;
562
+ }
563
+ deps[index] =
564
+ paramHint === undefined
565
+ ? this.resolveFromContext(
566
+ param.token as Token<unknown> | Constructor,
567
+ resolutionPath,
568
+ resolutionStack,
569
+ )
570
+ : this.resolve(
571
+ param.token as Token<unknown> | Constructor,
572
+ paramHint,
573
+ resolutionPath,
574
+ resolutionStack,
575
+ );
576
+ }
577
+ return deps;
578
+ }
579
+
580
+ private _resolveDescriptorDeps(
581
+ deps: ReadonlyArray<InjectionDescriptor>,
582
+ resolutionPath: Array<string>,
583
+ resolutionStack: Array<ResolutionFrame>,
584
+ ): Array<unknown> {
585
+ const resolved = new Array<unknown>(deps.length);
586
+ for (let index = 0; index < deps.length; index += 1) {
587
+ const dep = deps[index]!;
588
+ const depHint = injectionSlotToResolveOptions(dep);
589
+ if (dep.multi) {
590
+ resolved[index] = this.resolveAll(
591
+ dep.token as Token<unknown> | Constructor,
592
+ depHint,
593
+ resolutionPath,
594
+ resolutionStack,
595
+ );
596
+ continue;
597
+ }
598
+ if (dep.optional) {
599
+ resolved[index] = this.resolveOptional(
600
+ dep.token as Token<unknown> | Constructor,
601
+ depHint,
602
+ resolutionPath,
603
+ resolutionStack,
604
+ );
605
+ continue;
606
+ }
607
+ resolved[index] =
608
+ depHint === undefined
609
+ ? this.resolveFromContext(
610
+ dep.token as Token<unknown> | Constructor,
611
+ resolutionPath,
612
+ resolutionStack,
613
+ )
614
+ : this.resolve(
615
+ dep.token as Token<unknown> | Constructor,
616
+ depHint,
617
+ resolutionPath,
618
+ resolutionStack,
619
+ );
620
+ }
621
+ return resolved;
622
+ }
623
+
624
+ resolveOptional<const Value>(
625
+ token: Token<Value> | Constructor<Value>,
626
+ hint: ResolveOptions | undefined,
627
+ resolutionPath: Array<string>,
628
+ resolutionStack: Array<ResolutionFrame>,
629
+ ): Value | undefined {
630
+ if (this._findBinding(token, hint, resolutionPath, resolutionStack) === undefined) {
631
+ return undefined;
632
+ }
633
+ return this.resolve(token, hint, resolutionPath, resolutionStack);
634
+ }
635
+
636
+ resolveAll<const Value>(
637
+ token: Token<Value> | Constructor<Value>,
638
+ hint: ResolveOptions | undefined,
639
+ resolutionPath: Array<string>,
640
+ resolutionStack: Array<ResolutionFrame>,
641
+ ): Array<Value> {
642
+ if (hint?.name !== undefined && hint.tag === undefined && (hint.tags?.length ?? 0) === 0) {
643
+ const namedCandidates = this._getSimpleNamedBindingsFromChain(token, hint.name);
644
+ if (namedCandidates.length === 0) {
645
+ return [];
646
+ }
647
+ const resolved = new Array<Value>(namedCandidates.length);
648
+ for (let index = 0; index < namedCandidates.length; index += 1) {
649
+ resolved[index] = this._resolveCandidateSync(
650
+ namedCandidates[index] as Binding<Value>,
651
+ hint,
652
+ resolutionPath,
653
+ resolutionStack,
654
+ );
655
+ }
656
+ return resolved;
657
+ }
658
+
659
+ const allBindings = this._getAllBindingsFromChain(token);
660
+ if (allBindings.length === 0) {
661
+ return [];
662
+ }
663
+
664
+ const ctx = this._makeConstraintContext(resolutionPath, resolutionStack, hint);
665
+ const candidates = selectAllBindings(allBindings, hint, ctx);
666
+
667
+ const resolved = new Array<Value>(candidates.length);
668
+ for (let index = 0; index < candidates.length; index += 1) {
669
+ resolved[index] = this._resolveCandidateSync(
670
+ candidates[index] as Binding<Value>,
671
+ hint,
672
+ resolutionPath,
673
+ resolutionStack,
674
+ );
675
+ }
676
+ return resolved;
677
+ }
678
+
679
+ // ── Async resolve ──────────────────────────────────────────────────────────
680
+
681
+ resolveAsyncFromContext<const Value>(
682
+ token: Token<Value> | Constructor<Value>,
683
+ resolutionPath: Array<string>,
684
+ resolutionStack: Array<ResolutionFrame>,
685
+ ): Promise<Value> {
686
+ const fastBinding = this._registry.getFastDefault(token);
687
+ if (fastBinding !== undefined) {
688
+ if (fastBinding.kind === "alias") {
689
+ return this.resolveAsyncFromContext(
690
+ fastBinding.target as Token<Value> | Constructor<Value>,
691
+ resolutionPath,
692
+ resolutionStack,
693
+ );
694
+ }
695
+ const scope = (fastBinding as BindingWithScope).scope ?? "transient";
696
+ if (
697
+ scope === "transient" &&
698
+ (fastBinding.kind === "dynamic" || fastBinding.kind === "dynamic-async") &&
699
+ fastBinding.onActivation === undefined &&
700
+ (this._lifecycle.activationVersion === 0 ||
701
+ !this._lifecycle.hasActivationHandlers(fastBinding.token))
702
+ ) {
703
+ return this._resolveTransientDynamicAsyncFromContext(
704
+ fastBinding as Binding<Value> & { kind: "dynamic" | "dynamic-async" },
705
+ resolutionPath,
706
+ resolutionStack,
707
+ );
708
+ }
709
+ if (scope === "singleton" && this._scope.hasSingleton(fastBinding.id)) {
710
+ return Promise.resolve(this._scope.getSingleton<Value>(fastBinding.id));
711
+ }
712
+ if (scope === "scoped") {
713
+ if (!this._scope.isChild) {
714
+ return Promise.reject(
715
+ new MissingScopeContextError(this._getTokenName(fastBinding.token)),
716
+ );
717
+ }
718
+ if (this._scope.hasScoped(fastBinding.id)) {
719
+ return Promise.resolve(this._scope.getScoped<Value>(fastBinding.id));
720
+ }
721
+ }
722
+ return this._resolveBindingAsync(
723
+ fastBinding as Binding<Value>,
724
+ undefined,
725
+ resolutionPath,
726
+ resolutionStack,
727
+ );
728
+ }
729
+
730
+ return this.resolveAsync(token, undefined, resolutionPath, resolutionStack);
731
+ }
732
+
733
+ async resolveAsync<const Value>(
734
+ token: Token<Value> | Constructor<Value>,
735
+ hint: ResolveOptions | undefined,
736
+ resolutionPath: Array<string>,
737
+ resolutionStack: Array<ResolutionFrame>,
738
+ ): Promise<Value> {
739
+ const found = this._findBinding(token, hint, resolutionPath, resolutionStack);
740
+
741
+ if (found === undefined) {
742
+ const ownBindings = this._registry.getAll(token);
743
+ if (ownBindings.length > 0) {
744
+ throw new NoMatchingBindingError(
745
+ this._getTokenName(token),
746
+ hint ?? {},
747
+ this._getAvailableSlots(token),
748
+ );
749
+ }
750
+ throw new TokenNotBoundError(this._getTokenName(token));
751
+ }
752
+
753
+ const { binding, owner } = found;
754
+
755
+ if (binding.kind === "alias") {
756
+ return this.resolveAsync(
757
+ binding.target as Token<Value> | Constructor<Value>,
758
+ hint,
759
+ resolutionPath,
760
+ resolutionStack,
761
+ );
762
+ }
763
+
764
+ const scope = (binding as BindingWithScope).scope ?? "transient";
765
+
766
+ if (scope === "singleton" && owner !== this) {
767
+ return owner._resolveBindingAsync(
768
+ binding as Binding<Value>,
769
+ hint,
770
+ resolutionPath,
771
+ resolutionStack,
772
+ );
773
+ }
774
+
775
+ return this._resolveBindingAsync(
776
+ binding as Binding<Value>,
777
+ hint,
778
+ resolutionPath,
779
+ resolutionStack,
780
+ );
781
+ }
782
+
783
+ private async _resolveBindingAsync<const Value>(
784
+ binding: Binding<Value>,
785
+ hint: ResolveOptions | undefined,
786
+ resolutionPath: Array<string>,
787
+ resolutionStack: Array<ResolutionFrame>,
788
+ ): Promise<Value> {
789
+ if (
790
+ binding.kind === "constant" &&
791
+ binding.onActivation === undefined &&
792
+ !this._lifecycle.hasActivationHandlers(binding.token)
793
+ ) {
794
+ return binding.value;
795
+ }
796
+
797
+ const scope = (binding as BindingWithScope).scope ?? "transient";
798
+
799
+ // Singleton cache
800
+ if (scope === "singleton") {
801
+ if (this._scope.hasSingleton(binding.id)) {
802
+ return this._scope.getSingleton<Value>(binding.id);
803
+ }
804
+ // In-flight dedup
805
+ const inflight = this._scope.getInflight(binding.id);
806
+ if (inflight !== undefined) {
807
+ return inflight as Promise<Value>;
808
+ }
809
+ }
810
+
811
+ // Scoped cache
812
+ if (scope === "scoped") {
813
+ if (!this._scope.isChild) {
814
+ throw new MissingScopeContextError(this._getTokenName(binding.token));
815
+ }
816
+ if (this._scope.hasScoped(binding.id)) {
817
+ return this._scope.getScoped<Value>(binding.id);
818
+ }
819
+ }
820
+
821
+ const frame = this._getResolutionFrame(binding);
822
+ const frameName = frame.tokenName;
823
+ const pathWithSet = resolutionPath as ResolutionPathWithSet;
824
+ let resolutionSet = pathWithSet[RESOLUTION_SET_KEY];
825
+ if (resolutionSet === undefined && resolutionPath.length >= RESOLUTION_SET_THRESHOLD) {
826
+ resolutionSet = new Set<string>(resolutionPath);
827
+ pathWithSet[RESOLUTION_SET_KEY] = resolutionSet;
828
+ }
829
+
830
+ if (
831
+ resolutionSet !== undefined
832
+ ? resolutionSet.has(frameName)
833
+ : resolutionPath.includes(frameName)
834
+ ) {
835
+ throw new CircularDependencyError([...resolutionPath, frameName]);
836
+ }
837
+
838
+ resolutionPath.push(frameName);
839
+ resolutionSet?.add(frameName);
840
+ resolutionStack.push(frame);
841
+ const needsActivation = this._needsActivation(binding);
842
+ if (
843
+ !needsActivation &&
844
+ scope === "transient" &&
845
+ (binding.kind === "dynamic" || binding.kind === "dynamic-async")
846
+ ) {
847
+ const resolutionCtx = new DefaultResolutionContext(
848
+ this as unknown as ResolverCallbacks,
849
+ resolutionPath,
850
+ resolutionStack,
851
+ hint,
852
+ );
853
+ try {
854
+ if (binding.kind === "dynamic-async") {
855
+ return await binding.factory(resolutionCtx);
856
+ }
857
+ const dynamicResult = binding.factory(resolutionCtx);
858
+ return dynamicResult instanceof Promise ? await dynamicResult : dynamicResult;
859
+ } finally {
860
+ resolutionStack.pop();
861
+ resolutionPath.pop();
862
+ resolutionSet?.delete(frameName);
863
+ }
864
+ }
865
+
866
+ const needsResolutionContext = needsActivation || this._requiresResolutionContext(binding);
867
+ const resolutionCtx = needsResolutionContext
868
+ ? new DefaultResolutionContext(
869
+ this as unknown as ResolverCallbacks,
870
+ resolutionPath,
871
+ resolutionStack,
872
+ hint,
873
+ )
874
+ : undefined;
875
+
876
+ try {
877
+ if (scope === "singleton") {
878
+ const createSingletonPromise = async (): Promise<Value> => {
879
+ const instance = await this._instantiateAsync(
880
+ binding,
881
+ resolutionCtx,
882
+ resolutionPath,
883
+ resolutionStack,
884
+ );
885
+
886
+ const shouldActivate = this._refreshActivationCacheIfNeeded(binding, needsActivation);
887
+ const activated = shouldActivate
888
+ ? await this._lifecycle.runActivation(
889
+ resolutionCtx as DefaultResolutionContext,
890
+ binding,
891
+ instance,
892
+ this._metadataReader,
893
+ )
894
+ : instance;
895
+
896
+ this._scope.setSingleton(binding.id, activated);
897
+ this._scope.clearInflight(binding.id);
898
+ return activated;
899
+ };
900
+
901
+ const singletonPromise = createSingletonPromise().catch((err: unknown) => {
902
+ this._scope.clearInflight(binding.id);
903
+ throw err;
904
+ });
905
+ this._scope.setInflight(binding.id, singletonPromise as Promise<unknown>);
906
+ return await singletonPromise;
907
+ }
908
+
909
+ const instance = await this._instantiateAsync(
910
+ binding,
911
+ resolutionCtx,
912
+ resolutionPath,
913
+ resolutionStack,
914
+ );
915
+
916
+ const shouldActivate = this._refreshActivationCacheIfNeeded(binding, needsActivation);
917
+ const activated = shouldActivate
918
+ ? await this._lifecycle.runActivation(
919
+ resolutionCtx as DefaultResolutionContext,
920
+ binding,
921
+ instance,
922
+ this._metadataReader,
923
+ )
924
+ : instance;
925
+
926
+ if (scope === "scoped") {
927
+ this._scope.setScoped(binding.id, activated);
928
+ }
929
+
930
+ return activated;
931
+ } finally {
932
+ resolutionStack.pop();
933
+ resolutionPath.pop();
934
+ resolutionSet?.delete(frameName);
935
+ }
936
+ }
937
+
938
+ private async _instantiateAsync<const Value>(
939
+ binding: Binding<Value>,
940
+ ctx: DefaultResolutionContext | undefined,
941
+ resolutionPath: Array<string>,
942
+ resolutionStack: Array<ResolutionFrame>,
943
+ ): Promise<Value> {
944
+ switch (binding.kind) {
945
+ case "constant":
946
+ return binding.value;
947
+
948
+ case "dynamic": {
949
+ if (ctx === undefined) {
950
+ throw new InternalError("dynamic binding requires resolution context");
951
+ }
952
+ const factoryResult = binding.factory(ctx);
953
+ return factoryResult instanceof Promise ? factoryResult : Promise.resolve(factoryResult);
954
+ }
955
+
956
+ case "dynamic-async":
957
+ if (ctx === undefined) {
958
+ throw new InternalError("dynamic-async binding requires resolution context");
959
+ }
960
+ return binding.factory(ctx);
961
+
962
+ case "class": {
963
+ const deps = await this._resolveClassDepsAsync(
964
+ binding.target,
965
+ resolutionPath,
966
+ resolutionStack,
967
+ );
968
+ const instance = this._instantiateClass(binding.target, deps);
969
+ return instance as Value;
970
+ }
971
+
972
+ case "resolved": {
973
+ if (ctx === undefined) {
974
+ throw new InternalError("resolved binding requires resolution context");
975
+ }
976
+ const deps = await this._resolveDescriptorDepsAsync(
977
+ binding.deps,
978
+ resolutionPath,
979
+ resolutionStack,
980
+ );
981
+ const factoryResult = binding.factory(...deps);
982
+ return factoryResult instanceof Promise ? factoryResult : Promise.resolve(factoryResult);
983
+ }
984
+
985
+ case "resolved-async": {
986
+ const deps = await this._resolveDescriptorDepsAsync(
987
+ binding.deps,
988
+ resolutionPath,
989
+ resolutionStack,
990
+ );
991
+ return binding.factory(...deps);
992
+ }
993
+
994
+ case "alias":
995
+ throw new InternalError("alias should have been followed before instantiation");
996
+ }
997
+ }
998
+
999
+ private async _resolveClassDepsAsync(
1000
+ target: Constructor,
1001
+ resolutionPath: Array<string>,
1002
+ resolutionStack: Array<ResolutionFrame>,
1003
+ ): Promise<Array<unknown>> {
1004
+ const meta = this._getConstructorMetadata(target);
1005
+ if (meta === undefined) {
1006
+ if (target.length === 0) {
1007
+ return [];
1008
+ }
1009
+ throw new MissingMetadataError(target.name);
1010
+ }
1011
+ if (meta.params.length === 0) {
1012
+ return [];
1013
+ }
1014
+ if (meta.params.length === 1) {
1015
+ const param = meta.params[0]!;
1016
+ const paramHint = injectionSlotToResolveOptions(param);
1017
+ if (param.multi) {
1018
+ return [
1019
+ await this.resolveAllAsync(
1020
+ param.token as Token<unknown> | Constructor,
1021
+ paramHint,
1022
+ resolutionPath,
1023
+ resolutionStack,
1024
+ ),
1025
+ ];
1026
+ }
1027
+ if (param.optional) {
1028
+ return [
1029
+ await this.resolveOptionalAsync(
1030
+ param.token as Token<unknown> | Constructor,
1031
+ paramHint,
1032
+ resolutionPath,
1033
+ resolutionStack,
1034
+ ),
1035
+ ];
1036
+ }
1037
+ if (paramHint === undefined) {
1038
+ return [
1039
+ await this.resolveAsyncFromContext(
1040
+ param.token as Token<unknown> | Constructor,
1041
+ resolutionPath,
1042
+ resolutionStack,
1043
+ ),
1044
+ ];
1045
+ }
1046
+ return [
1047
+ await this.resolveAsync(
1048
+ param.token as Token<unknown> | Constructor,
1049
+ paramHint,
1050
+ resolutionPath,
1051
+ resolutionStack,
1052
+ ),
1053
+ ];
1054
+ }
1055
+ const pending = new Array<Promise<unknown>>(meta.params.length);
1056
+ const shouldCloneContext = meta.params.length > 1;
1057
+ for (let index = 0; index < meta.params.length; index += 1) {
1058
+ const param = meta.params[index]!;
1059
+ const paramHint = injectionSlotToResolveOptions(param);
1060
+ if (param.multi) {
1061
+ pending[index] = this.resolveAllAsync(
1062
+ param.token as Token<unknown> | Constructor,
1063
+ paramHint,
1064
+ shouldCloneContext ? [...resolutionPath] : resolutionPath,
1065
+ shouldCloneContext ? [...resolutionStack] : resolutionStack,
1066
+ );
1067
+ } else if (param.optional) {
1068
+ pending[index] = this.resolveOptionalAsync(
1069
+ param.token as Token<unknown> | Constructor,
1070
+ paramHint,
1071
+ shouldCloneContext ? [...resolutionPath] : resolutionPath,
1072
+ shouldCloneContext ? [...resolutionStack] : resolutionStack,
1073
+ );
1074
+ } else {
1075
+ pending[index] =
1076
+ paramHint === undefined
1077
+ ? this.resolveAsyncFromContext(
1078
+ param.token as Token<unknown> | Constructor,
1079
+ shouldCloneContext ? [...resolutionPath] : resolutionPath,
1080
+ shouldCloneContext ? [...resolutionStack] : resolutionStack,
1081
+ )
1082
+ : this.resolveAsync(
1083
+ param.token as Token<unknown> | Constructor,
1084
+ paramHint,
1085
+ shouldCloneContext ? [...resolutionPath] : resolutionPath,
1086
+ shouldCloneContext ? [...resolutionStack] : resolutionStack,
1087
+ );
1088
+ }
1089
+ }
1090
+ return Promise.all(pending);
1091
+ }
1092
+
1093
+ private async _resolveDescriptorDepsAsync(
1094
+ deps: ReadonlyArray<InjectionDescriptor>,
1095
+ resolutionPath: Array<string>,
1096
+ resolutionStack: Array<ResolutionFrame>,
1097
+ ): Promise<Array<unknown>> {
1098
+ const pending = new Array<Promise<unknown>>(deps.length);
1099
+ const shouldCloneContext = deps.length > 1;
1100
+ for (let index = 0; index < deps.length; index += 1) {
1101
+ const dep = deps[index]!;
1102
+ const depHint = injectionSlotToResolveOptions(dep);
1103
+ if (dep.multi) {
1104
+ pending[index] = this.resolveAllAsync(
1105
+ dep.token as Token<unknown> | Constructor,
1106
+ depHint,
1107
+ shouldCloneContext ? [...resolutionPath] : resolutionPath,
1108
+ shouldCloneContext ? [...resolutionStack] : resolutionStack,
1109
+ );
1110
+ } else if (dep.optional) {
1111
+ pending[index] = this.resolveOptionalAsync(
1112
+ dep.token as Token<unknown> | Constructor,
1113
+ depHint,
1114
+ shouldCloneContext ? [...resolutionPath] : resolutionPath,
1115
+ shouldCloneContext ? [...resolutionStack] : resolutionStack,
1116
+ );
1117
+ } else {
1118
+ pending[index] =
1119
+ depHint === undefined
1120
+ ? this.resolveAsyncFromContext(
1121
+ dep.token as Token<unknown> | Constructor,
1122
+ shouldCloneContext ? [...resolutionPath] : resolutionPath,
1123
+ shouldCloneContext ? [...resolutionStack] : resolutionStack,
1124
+ )
1125
+ : this.resolveAsync(
1126
+ dep.token as Token<unknown> | Constructor,
1127
+ depHint,
1128
+ shouldCloneContext ? [...resolutionPath] : resolutionPath,
1129
+ shouldCloneContext ? [...resolutionStack] : resolutionStack,
1130
+ );
1131
+ }
1132
+ }
1133
+ return Promise.all(pending);
1134
+ }
1135
+
1136
+ async resolveOptionalAsync<const Value>(
1137
+ token: Token<Value> | Constructor<Value>,
1138
+ hint: ResolveOptions | undefined,
1139
+ resolutionPath: Array<string>,
1140
+ resolutionStack: Array<ResolutionFrame>,
1141
+ ): Promise<Value | undefined> {
1142
+ if (this._findBinding(token, hint, resolutionPath, resolutionStack) === undefined) {
1143
+ return undefined;
1144
+ }
1145
+ return this.resolveAsync(token, hint, resolutionPath, resolutionStack);
1146
+ }
1147
+
1148
+ async resolveAllAsync<const Value>(
1149
+ token: Token<Value> | Constructor<Value>,
1150
+ hint: ResolveOptions | undefined,
1151
+ resolutionPath: Array<string>,
1152
+ resolutionStack: Array<ResolutionFrame>,
1153
+ ): Promise<Array<Value>> {
1154
+ if (hint?.name !== undefined && hint.tag === undefined && (hint.tags?.length ?? 0) === 0) {
1155
+ const namedCandidates = this._getSimpleNamedBindingsFromChain(token, hint.name);
1156
+ if (namedCandidates.length === 0) {
1157
+ return [];
1158
+ }
1159
+ const pending = new Array<Promise<Value>>(namedCandidates.length);
1160
+ for (let index = 0; index < namedCandidates.length; index += 1) {
1161
+ pending[index] = this._resolveCandidateAsync(
1162
+ namedCandidates[index] as Binding<Value>,
1163
+ hint,
1164
+ resolutionPath,
1165
+ resolutionStack,
1166
+ );
1167
+ }
1168
+ return Promise.all(pending);
1169
+ }
1170
+
1171
+ const allBindings = this._getAllBindingsFromChain(token);
1172
+ if (allBindings.length === 0) {
1173
+ return [];
1174
+ }
1175
+
1176
+ const ctx = this._makeConstraintContext(resolutionPath, resolutionStack, hint);
1177
+ const candidates = selectAllBindings(allBindings, hint, ctx);
1178
+
1179
+ const pending = new Array<Promise<Value>>(candidates.length);
1180
+ for (let index = 0; index < candidates.length; index += 1) {
1181
+ pending[index] = this._resolveCandidateAsync(
1182
+ candidates[index] as Binding<Value>,
1183
+ hint,
1184
+ resolutionPath,
1185
+ resolutionStack,
1186
+ );
1187
+ }
1188
+ return Promise.all(pending);
1189
+ }
1190
+
1191
+ // ── Helpers ────────────────────────────────────────────────────────────────
1192
+
1193
+ private _getAllBindingsFromChain(token: Token<unknown> | Constructor): ReadonlyArray<Binding> {
1194
+ const ownBindings = this._registry.getAll(token);
1195
+ if (this._parent === undefined) {
1196
+ return ownBindings;
1197
+ }
1198
+ const result: Array<Binding> = [...ownBindings];
1199
+ let current: DependencyResolver | undefined = this._parent;
1200
+ while (current !== undefined) {
1201
+ const own = current._registry.getAll(token);
1202
+ if (own.length > 0) {
1203
+ result.push(...own);
1204
+ }
1205
+ current = current._parent;
1206
+ }
1207
+ return result;
1208
+ }
1209
+
1210
+ private _getSimpleNamedBindingsFromChain(
1211
+ token: Token<unknown> | Constructor,
1212
+ name: string,
1213
+ ): Array<Binding> {
1214
+ const ownBinding = this._registry.getSimpleNamed(token, name);
1215
+ if (this._parent === undefined) {
1216
+ return ownBinding !== undefined ? [ownBinding] : [];
1217
+ }
1218
+ const result: Array<Binding> = [];
1219
+ if (ownBinding !== undefined) {
1220
+ result.push(ownBinding);
1221
+ }
1222
+ let current: DependencyResolver | undefined = this._parent;
1223
+ while (current !== undefined) {
1224
+ const binding = current._registry.getSimpleNamed(token, name);
1225
+ if (binding !== undefined) {
1226
+ result.push(binding);
1227
+ }
1228
+ current = current._parent;
1229
+ }
1230
+ return result;
1231
+ }
1232
+
1233
+ private _getAvailableSlots(token: Token<unknown> | Constructor): Array<string> {
1234
+ return this._registry.availableSlotStrings(token);
1235
+ }
1236
+
1237
+ private _makeConstraintContext(
1238
+ resolutionPath: Array<string>,
1239
+ resolutionStack: Array<ResolutionFrame>,
1240
+ hint: ResolveOptions | undefined,
1241
+ ): ConstraintContext {
1242
+ if (hint === undefined && resolutionPath.length === 0 && resolutionStack.length === 0) {
1243
+ return ROOT_CONSTRAINT_CONTEXT;
1244
+ }
1245
+ const parent = resolutionStack.at(-1);
1246
+ const ancestors = resolutionStack.length > 1 ? resolutionStack.slice(0, -1) : [];
1247
+ return {
1248
+ resolutionPath,
1249
+ resolutionStack,
1250
+ parent,
1251
+ ancestors,
1252
+ currentResolveHint: hint,
1253
+ };
1254
+ }
1255
+
1256
+ private _matchesBindingFast(
1257
+ binding: Binding,
1258
+ hint: ResolveOptions | undefined,
1259
+ resolutionPath: Array<string>,
1260
+ resolutionStack: Array<ResolutionFrame>,
1261
+ ): boolean {
1262
+ if (!this._matchesSlotFast(binding.slot, hint)) {
1263
+ return false;
1264
+ }
1265
+ if (binding.predicate === undefined) {
1266
+ return true;
1267
+ }
1268
+ const ctx = this._makeConstraintContext(resolutionPath, resolutionStack, hint);
1269
+ return binding.predicate(ctx);
1270
+ }
1271
+
1272
+ private _matchesSlotFast(slot: BindingSlot, hint: ResolveOptions | undefined): boolean {
1273
+ const hintName = hint?.name;
1274
+ const hintTags = hint?.tags;
1275
+ const singleHintTag = hint?.tag;
1276
+ const hasHintTags = (hintTags?.length ?? 0) > 0 || singleHintTag !== undefined;
1277
+
1278
+ if (slot.name !== undefined) {
1279
+ if (hintName === undefined || slot.name !== hintName) {
1280
+ return false;
1281
+ }
1282
+ } else if (hintName !== undefined) {
1283
+ return false;
1284
+ }
1285
+
1286
+ if (slot.tags.length > 0) {
1287
+ if (!hasHintTags) {
1288
+ return false;
1289
+ }
1290
+ for (const [tagKey, tagValue] of slot.tags) {
1291
+ if (!this._matchesHintTag(tagKey, tagValue, hintTags, singleHintTag)) {
1292
+ return false;
1293
+ }
1294
+ }
1295
+ } else if (hasHintTags) {
1296
+ return false;
1297
+ }
1298
+
1299
+ return true;
1300
+ }
1301
+
1302
+ private _getTokenName(token: Token<unknown> | Constructor): string {
1303
+ return tokenName(token);
1304
+ }
1305
+
1306
+ private _getConstructorMetadata(target: Constructor): ConstructorMetadata | undefined {
1307
+ const cached = this._classConstructorMetadata.get(target);
1308
+ if (cached !== undefined) {
1309
+ return cached === null ? undefined : cached;
1310
+ }
1311
+ const metadata = this._metadataReader.getConstructorMetadata(target);
1312
+ this._classConstructorMetadata.set(target, metadata ?? null);
1313
+ return metadata;
1314
+ }
1315
+
1316
+ private _instantiateClass(target: Constructor, deps: Array<unknown>): unknown {
1317
+ let needsActiveContainer = this._classNeedsActiveContainer.get(target);
1318
+ if (needsActiveContainer === undefined) {
1319
+ const accessorMetadata = this._metadataReader.getAccessorMetadata?.(target);
1320
+ needsActiveContainer = (accessorMetadata?.length ?? 0) > 0;
1321
+ this._classNeedsActiveContainer.set(target, needsActiveContainer);
1322
+ }
1323
+ const invokable = target as ConstructorInvocation;
1324
+ if (!needsActiveContainer) {
1325
+ return new invokable(...deps);
1326
+ }
1327
+ return runWithContainer(this._container, () => new invokable(...deps));
1328
+ }
1329
+
1330
+ private _matchesHintTag(
1331
+ tagKey: string,
1332
+ tagValue: unknown,
1333
+ hintTags: ReadonlyArray<BindingTag> | undefined,
1334
+ singleHintTag: BindingTag | undefined,
1335
+ ): boolean {
1336
+ if (
1337
+ singleHintTag !== undefined &&
1338
+ singleHintTag[0] === tagKey &&
1339
+ Object.is(singleHintTag[1], tagValue)
1340
+ ) {
1341
+ return true;
1342
+ }
1343
+ if (hintTags === undefined || hintTags.length === 0) {
1344
+ return false;
1345
+ }
1346
+ for (let index = 0; index < hintTags.length; index += 1) {
1347
+ const hintTag = hintTags[index]!;
1348
+ if (hintTag[0] === tagKey && Object.is(hintTag[1], tagValue)) {
1349
+ return true;
1350
+ }
1351
+ }
1352
+ return false;
1353
+ }
1354
+
1355
+ private _resolveTransientDynamicSyncFromContext<const Value>(
1356
+ binding: Binding<Value> & { kind: "dynamic" },
1357
+ resolutionPath: Array<string>,
1358
+ resolutionStack: Array<ResolutionFrame>,
1359
+ ): Value {
1360
+ // ── Shallow path (depth < RESOLUTION_SET_THRESHOLD) ──────────────────────────────────
1361
+ // Use resolutionPath.includes for cycle detection: for tiny arrays (depth 0–31) this is
1362
+ // faster than a Set lookup. Keep resolutionStack push/pop and the per-depth pool so
1363
+ // ctx.graph reflects correct state for shallow-chain factories.
1364
+ if (resolutionPath.length < RESOLUTION_SET_THRESHOLD) {
1365
+ const frame = this._getResolutionFrame(binding);
1366
+ const tokenDisplayName = frame.tokenName;
1367
+ if (resolutionPath.includes(tokenDisplayName)) {
1368
+ throw new CircularDependencyError([...resolutionPath, tokenDisplayName]);
1369
+ }
1370
+ resolutionPath.push(tokenDisplayName);
1371
+ resolutionStack.push(frame);
1372
+ const resolutionCtx = this._acquireSyncResolutionContext(
1373
+ resolutionPath,
1374
+ resolutionStack,
1375
+ undefined,
1376
+ );
1377
+ try {
1378
+ const dynamicResult = binding.factory(resolutionCtx);
1379
+ if (dynamicResult instanceof Promise) {
1380
+ throw new AsyncResolutionError(tokenDisplayName, tokenDisplayName);
1381
+ }
1382
+ return dynamicResult;
1383
+ } finally {
1384
+ resolutionStack.pop();
1385
+ resolutionPath.pop();
1386
+ }
1387
+ }
1388
+
1389
+ // ── Deep path (depth >= RESOLUTION_SET_THRESHOLD) ────────────────────────────────────
1390
+ // At this depth the O(N) array scan becomes expensive. Switch to a class-level
1391
+ // Set<BindingIdentifier> for O(1) cycle detection.
1392
+ //
1393
+ // Performance decisions vs. shallow path:
1394
+ // 1. _getResolutionFrame is NOT called: frameName is only needed for resolutionPath.push
1395
+ // and error messages. Both are handled below without the frame Map lookup.
1396
+ // 2. resolutionPath.push / pop is SKIPPED: cycle detection uses _deepCycleIds (binding IDs),
1397
+ // so path membership tracking through the string array is unnecessary. Eliminating
1398
+ // ~480 array writes per 512-chain avoids GC write-barriers on every level.
1399
+ // Trade-off: CircularDependencyError thrown for a deep cycle (depth > 32) will only
1400
+ // include the first 32 path elements in its message; levels 32+ are omitted.
1401
+ // 3. The shared context is set up ONCE (when _deepActiveLevels === 0) rather than
1402
+ // re-checked on every level — saves two property reads + a reference comparison
1403
+ // for each of the ~480 subsequent deep-chain levels.
1404
+ //
1405
+ // Reentrancy: if a *different* deep chain is currently active (factory called
1406
+ // container.resolve() internally and that inner chain also reached the threshold),
1407
+ // fall back to the slow path to avoid cross-chain Set pollution.
1408
+ if (this._deepActiveLevels > 0 && this._deepSyncCtxPath !== resolutionPath) {
1409
+ return this._resolveTransientDynamicSyncSlow(binding, resolutionPath, resolutionStack);
1410
+ }
1411
+
1412
+ // First deep level: bump the generation (implicitly clearing all stale cycle marks from
1413
+ // previous chains), seed the marks with the shallow-path frames already on the
1414
+ // resolution stack, then initialise (or reset) the shared context once for the
1415
+ // whole deep chain.
1416
+ if (this._deepActiveLevels === 0) {
1417
+ const gen = ++this._deepCycleGen;
1418
+ for (const stackFrame of resolutionStack) {
1419
+ this._deepCycleMarks.set(stackFrame.bindingId, gen);
1420
+ }
1421
+ let ctx = this._deepSyncCtx;
1422
+ if (ctx === undefined) {
1423
+ ctx = new DefaultResolutionContext(
1424
+ this as unknown as ResolverCallbacks,
1425
+ resolutionPath,
1426
+ resolutionStack,
1427
+ undefined,
1428
+ );
1429
+ this._deepSyncCtx = ctx;
1430
+ } else {
1431
+ ctx.reset(this as unknown as ResolverCallbacks, resolutionPath, resolutionStack, undefined);
1432
+ }
1433
+ this._deepSyncCtxPath = resolutionPath;
1434
+ }
1435
+
1436
+ if (this._deepCycleMarks.get(binding.id) === this._deepCycleGen) {
1437
+ throw new CircularDependencyError([...resolutionPath, tokenName(binding.token)]);
1438
+ }
1439
+
1440
+ this._deepCycleMarks.set(binding.id, this._deepCycleGen);
1441
+ this._deepActiveLevels++;
1442
+
1443
+ try {
1444
+ // Use the pre-initialised context directly — no local variable needed.
1445
+ const dynamicResult = binding.factory(this._deepSyncCtx!);
1446
+ if (dynamicResult instanceof Promise) {
1447
+ throw new AsyncResolutionError(tokenName(binding.token), tokenName(binding.token));
1448
+ }
1449
+ return dynamicResult;
1450
+ } finally {
1451
+ // No Map.delete needed: the generation counter makes old marks invisible.
1452
+ // Only reset the path pointer when the last deep level unwinds.
1453
+ if (--this._deepActiveLevels === 0) {
1454
+ this._deepSyncCtxPath = undefined;
1455
+ }
1456
+ }
1457
+ }
1458
+
1459
+ private _resolveTransientDynamicSyncSlow<const Value>(
1460
+ binding: Binding<Value> & { kind: "dynamic" },
1461
+ resolutionPath: Array<string>,
1462
+ resolutionStack: Array<ResolutionFrame>,
1463
+ ): Value {
1464
+ // Rare fallback used when deep-chain reentrancy is detected (a factory called
1465
+ // container.resolve() directly and the resulting chain also reached the threshold).
1466
+ const frame = this._getResolutionFrame(binding);
1467
+ const tokenDisplayName = frame.tokenName;
1468
+ const pathWithSet = resolutionPath as ResolutionPathWithSet;
1469
+ let resolutionSet = pathWithSet[RESOLUTION_SET_KEY];
1470
+ if (resolutionSet === undefined) {
1471
+ resolutionSet = new Set<string>(resolutionPath);
1472
+ pathWithSet[RESOLUTION_SET_KEY] = resolutionSet;
1473
+ }
1474
+ if (resolutionSet.has(tokenDisplayName)) {
1475
+ throw new CircularDependencyError([...resolutionPath, tokenDisplayName]);
1476
+ }
1477
+ resolutionPath.push(tokenDisplayName);
1478
+ resolutionSet.add(tokenDisplayName);
1479
+ resolutionStack.push(frame);
1480
+ const resolutionCtx = this._acquireSyncResolutionContext(
1481
+ resolutionPath,
1482
+ resolutionStack,
1483
+ undefined,
1484
+ );
1485
+ try {
1486
+ const dynamicResult = binding.factory(resolutionCtx);
1487
+ if (dynamicResult instanceof Promise) {
1488
+ throw new AsyncResolutionError(tokenDisplayName, tokenDisplayName);
1489
+ }
1490
+ return dynamicResult;
1491
+ } finally {
1492
+ resolutionStack.pop();
1493
+ resolutionPath.pop();
1494
+ resolutionSet.delete(tokenDisplayName);
1495
+ }
1496
+ }
1497
+
1498
+ // NOT declared `async` — avoids creating a JSAsyncGeneratorObject + implicit Promise wrapper on
1499
+ // every invocation. Cleanup is handled via .then(onFulfilled, onRejected) so the behaviour is
1500
+ // identical to a try/finally but without the async machinery overhead.
1501
+ private _resolveTransientDynamicAsyncFromContext<const Value>(
1502
+ binding: Binding<Value> & { kind: "dynamic" | "dynamic-async" },
1503
+ resolutionPath: Array<string>,
1504
+ resolutionStack: Array<ResolutionFrame>,
1505
+ ): Promise<Value> {
1506
+ // ── Shallow async path (depth < RESOLUTION_SET_THRESHOLD) ─────────────────────────────
1507
+ // For the common case of a sequential async chain (each factory awaits one dependency at a
1508
+ // time), all levels share the same resolutionPath/resolutionStack arrays. A single
1509
+ // DefaultResolutionContext can therefore serve the entire chain:
1510
+ // • levels 2-N of the owning chain: zero allocation, zero reset writes
1511
+ // • concurrent chains (Promise.all roots): detected by path-identity mismatch → fallback
1512
+ // to a fresh DefaultResolutionContext for that level only
1513
+ // resolutionStack is NOT pushed here — see class-level comment for the trade-off.
1514
+ if (resolutionPath.length < RESOLUTION_SET_THRESHOLD) {
1515
+ const tokenDisplayName = tokenName(binding.token);
1516
+ if (resolutionPath.includes(tokenDisplayName)) {
1517
+ return Promise.reject(new CircularDependencyError([...resolutionPath, tokenDisplayName]));
1518
+ }
1519
+
1520
+ resolutionPath.push(tokenDisplayName);
1521
+
1522
+ // Determine which context to use and whether this level owns the shared context.
1523
+ let ctx: DefaultResolutionContext;
1524
+ let isOwnerLevel: boolean;
1525
+ if (this._deepAsyncCtxPath === resolutionPath) {
1526
+ // Inner level of the owning chain — reuse the shared context with NO setup overhead.
1527
+ ctx = this._deepAsyncCtx!;
1528
+ isOwnerLevel = true;
1529
+ } else if (this._deepAsyncCtxPath === undefined) {
1530
+ // Root of a new chain — take ownership and initialise (or reset) the shared context.
1531
+ const existing = this._deepAsyncCtx;
1532
+ if (existing === undefined) {
1533
+ ctx = new DefaultResolutionContext(
1534
+ this as unknown as ResolverCallbacks,
1535
+ resolutionPath,
1536
+ resolutionStack,
1537
+ undefined,
1538
+ );
1539
+ this._deepAsyncCtx = ctx;
1540
+ } else {
1541
+ existing.reset(
1542
+ this as unknown as ResolverCallbacks,
1543
+ resolutionPath,
1544
+ resolutionStack,
1545
+ undefined,
1546
+ );
1547
+ ctx = existing;
1548
+ }
1549
+ this._deepAsyncCtxPath = resolutionPath;
1550
+ isOwnerLevel = true;
1551
+ } else {
1552
+ // Concurrent chain (e.g. Promise.all) — allocate a dedicated context and do NOT
1553
+ // interfere with the owning chain's state.
1554
+ ctx = new DefaultResolutionContext(
1555
+ this as unknown as ResolverCallbacks,
1556
+ resolutionPath,
1557
+ resolutionStack,
1558
+ undefined,
1559
+ );
1560
+ isOwnerLevel = false;
1561
+ }
1562
+
1563
+ if (isOwnerLevel) {
1564
+ this._deepAsyncActiveLevels++;
1565
+ }
1566
+
1567
+ // Invoke the factory synchronously to get its Promise (or a resolved value for "dynamic").
1568
+ let factoryPromise: Promise<Value>;
1569
+ try {
1570
+ if (binding.kind === "dynamic-async") {
1571
+ factoryPromise = binding.factory(ctx);
1572
+ } else {
1573
+ const factoryResult = binding.factory(ctx);
1574
+ factoryPromise =
1575
+ factoryResult instanceof Promise
1576
+ ? (factoryResult as Promise<Value>)
1577
+ : Promise.resolve(factoryResult);
1578
+ }
1579
+ } catch (err) {
1580
+ // Synchronous throw from the factory (rare) — clean up immediately.
1581
+ resolutionPath.pop();
1582
+ if (isOwnerLevel && --this._deepAsyncActiveLevels === 0) {
1583
+ this._deepAsyncCtxPath = undefined;
1584
+ }
1585
+ return Promise.reject(err);
1586
+ }
1587
+
1588
+ // Chain cleanup onto the Promise so it runs whether the factory resolves or rejects.
1589
+ return factoryPromise.then(
1590
+ (value) => {
1591
+ resolutionPath.pop();
1592
+ if (isOwnerLevel && --this._deepAsyncActiveLevels === 0) {
1593
+ this._deepAsyncCtxPath = undefined;
1594
+ }
1595
+ return value;
1596
+ },
1597
+ (err: unknown) => {
1598
+ resolutionPath.pop();
1599
+ if (isOwnerLevel && --this._deepAsyncActiveLevels === 0) {
1600
+ this._deepAsyncCtxPath = undefined;
1601
+ }
1602
+ // Re-throw as the rejected value; the `never` cast suppresses the TS return-type
1603
+ // mismatch that arises because `throw` has type `never` in an expression context.
1604
+ throw err as never;
1605
+ },
1606
+ );
1607
+ }
1608
+
1609
+ // ── Deep async path (depth ≥ RESOLUTION_SET_THRESHOLD) ────────────────────────────────
1610
+ // Fall back to the fully-correct slow implementation.
1611
+ return this._resolveTransientDynamicAsyncSlow(binding, resolutionPath, resolutionStack);
1612
+ }
1613
+
1614
+ private async _resolveTransientDynamicAsyncSlow<const Value>(
1615
+ binding: Binding<Value> & { kind: "dynamic" | "dynamic-async" },
1616
+ resolutionPath: Array<string>,
1617
+ resolutionStack: Array<ResolutionFrame>,
1618
+ ): Promise<Value> {
1619
+ const frame = this._getResolutionFrame(binding);
1620
+ const tokenDisplayName = frame.tokenName;
1621
+ const pathWithSet = resolutionPath as ResolutionPathWithSet;
1622
+ let resolutionSet = pathWithSet[RESOLUTION_SET_KEY];
1623
+ if (resolutionSet === undefined) {
1624
+ resolutionSet = new Set<string>(resolutionPath);
1625
+ pathWithSet[RESOLUTION_SET_KEY] = resolutionSet;
1626
+ }
1627
+ if (resolutionSet.has(tokenDisplayName)) {
1628
+ throw new CircularDependencyError([...resolutionPath, tokenDisplayName]);
1629
+ }
1630
+ resolutionPath.push(tokenDisplayName);
1631
+ resolutionSet.add(tokenDisplayName);
1632
+ resolutionStack.push(frame);
1633
+ const resolutionCtx = new DefaultResolutionContext(
1634
+ this as unknown as ResolverCallbacks,
1635
+ resolutionPath,
1636
+ resolutionStack,
1637
+ undefined,
1638
+ );
1639
+ try {
1640
+ if (binding.kind === "dynamic-async") {
1641
+ return await binding.factory(resolutionCtx);
1642
+ }
1643
+ const dynamicResult = binding.factory(resolutionCtx);
1644
+ return dynamicResult instanceof Promise ? await dynamicResult : dynamicResult;
1645
+ } finally {
1646
+ resolutionStack.pop();
1647
+ resolutionPath.pop();
1648
+ resolutionSet.delete(tokenDisplayName);
1649
+ }
1650
+ }
1651
+
1652
+ private _resolveCandidateSync<const Value>(
1653
+ binding: Binding<Value>,
1654
+ hint: ResolveOptions | undefined,
1655
+ resolutionPath: Array<string>,
1656
+ resolutionStack: Array<ResolutionFrame>,
1657
+ ): Value {
1658
+ if (
1659
+ binding.kind === "constant" &&
1660
+ binding.onActivation === undefined &&
1661
+ !this._lifecycle.hasActivationHandlers(binding.token)
1662
+ ) {
1663
+ return binding.value;
1664
+ }
1665
+ if (binding.kind === "alias") {
1666
+ return this.resolve(
1667
+ binding.target as Token<Value> | Constructor<Value>,
1668
+ hint,
1669
+ resolutionPath,
1670
+ resolutionStack,
1671
+ );
1672
+ }
1673
+ const scope = (binding as BindingWithScope).scope ?? "transient";
1674
+ if (scope === "singleton" && this._scope.hasSingleton(binding.id)) {
1675
+ return this._scope.getSingleton<Value>(binding.id);
1676
+ }
1677
+ if (scope === "scoped") {
1678
+ if (!this._scope.isChild) {
1679
+ throw new MissingScopeContextError(this._getTokenName(binding.token));
1680
+ }
1681
+ if (this._scope.hasScoped(binding.id)) {
1682
+ return this._scope.getScoped<Value>(binding.id);
1683
+ }
1684
+ }
1685
+ return this._resolveBinding(binding, hint, resolutionPath, resolutionStack);
1686
+ }
1687
+
1688
+ private _resolveCandidateAsync<const Value>(
1689
+ binding: Binding<Value>,
1690
+ hint: ResolveOptions | undefined,
1691
+ resolutionPath: Array<string>,
1692
+ resolutionStack: Array<ResolutionFrame>,
1693
+ ): Promise<Value> {
1694
+ if (
1695
+ binding.kind === "constant" &&
1696
+ binding.onActivation === undefined &&
1697
+ !this._lifecycle.hasActivationHandlers(binding.token)
1698
+ ) {
1699
+ return Promise.resolve(binding.value);
1700
+ }
1701
+ const isolatedPath = [...resolutionPath];
1702
+ const isolatedStack = [...resolutionStack];
1703
+ if (binding.kind === "alias") {
1704
+ return this.resolveAsync(
1705
+ binding.target as Token<Value> | Constructor<Value>,
1706
+ hint,
1707
+ isolatedPath,
1708
+ isolatedStack,
1709
+ );
1710
+ }
1711
+ const scope = (binding as BindingWithScope).scope ?? "transient";
1712
+ if (scope === "singleton" && this._scope.hasSingleton(binding.id)) {
1713
+ return Promise.resolve(this._scope.getSingleton<Value>(binding.id));
1714
+ }
1715
+ if (scope === "scoped") {
1716
+ if (!this._scope.isChild) {
1717
+ return Promise.reject(new MissingScopeContextError(this._getTokenName(binding.token)));
1718
+ }
1719
+ if (this._scope.hasScoped(binding.id)) {
1720
+ return Promise.resolve(this._scope.getScoped<Value>(binding.id));
1721
+ }
1722
+ }
1723
+ return this._resolveBindingAsync(binding, hint, isolatedPath, isolatedStack);
1724
+ }
1725
+
1726
+ private _getResolutionFrame<const Value>(binding: Binding<Value>): ResolutionFrame {
1727
+ const existing = this._frameByBindingId.get(binding.id);
1728
+ if (existing !== undefined) {
1729
+ return existing;
1730
+ }
1731
+ const scope = (binding as BindingWithScope).scope ?? "transient";
1732
+ const frame = buildResolutionFrame(
1733
+ tokenName(binding.token),
1734
+ scope,
1735
+ binding.id,
1736
+ binding.kind,
1737
+ binding.slot,
1738
+ );
1739
+ this._frameByBindingId.set(binding.id, frame);
1740
+ return frame;
1741
+ }
1742
+
1743
+ private _needsActivation<const Value>(binding: Binding<Value>): boolean {
1744
+ const lifecycleVersion = this._lifecycle.activationVersion;
1745
+ if (
1746
+ lifecycleVersion === 0 &&
1747
+ binding.kind !== "class" &&
1748
+ binding.kind !== "alias" &&
1749
+ binding.onActivation === undefined
1750
+ ) {
1751
+ return false;
1752
+ }
1753
+ if (this._activationCacheVersion !== lifecycleVersion) {
1754
+ this._activationNeedByBindingId.clear();
1755
+ this._activationCacheVersion = lifecycleVersion;
1756
+ }
1757
+
1758
+ const cached = this._activationNeedByBindingId.get(binding.id);
1759
+ if (cached !== undefined) {
1760
+ return cached;
1761
+ }
1762
+
1763
+ if (binding.kind === "class") {
1764
+ let hasActivation =
1765
+ this._lifecycle.hasActivationHandlers(binding.token) || binding.onActivation !== undefined;
1766
+ const cachedPostConstruct = this._classHasPostConstruct.get(binding.target);
1767
+ // Unknown class lifecycle metadata: activate once, then cache after first instantiation.
1768
+ if (cachedPostConstruct === undefined) {
1769
+ hasActivation = true;
1770
+ } else if (cachedPostConstruct) {
1771
+ hasActivation = true;
1772
+ }
1773
+ this._activationNeedByBindingId.set(binding.id, hasActivation);
1774
+ return hasActivation;
1775
+ }
1776
+
1777
+ let hasActivation = false;
1778
+ if (binding.kind !== "alias" && binding.onActivation !== undefined) {
1779
+ hasActivation = true;
1780
+ } else if (this._lifecycle.hasActivationHandlers(binding.token)) {
1781
+ hasActivation = true;
1782
+ }
1783
+
1784
+ this._activationNeedByBindingId.set(binding.id, hasActivation);
1785
+ return hasActivation;
1786
+ }
1787
+
1788
+ private _refreshClassPostConstructCache(target: Constructor): void {
1789
+ const lifecycle = this._metadataReader.getLifecycleMetadata(target);
1790
+ const hasPostConstruct =
1791
+ lifecycle !== undefined &&
1792
+ lifecycle.postConstruct !== undefined &&
1793
+ lifecycle.postConstruct.length > 0;
1794
+ this._classHasPostConstruct.set(target, hasPostConstruct);
1795
+ }
1796
+
1797
+ /**
1798
+ * Refreshes the post-construct cache for class bindings on first instantiation and
1799
+ * returns the (possibly updated) shouldActivate flag.
1800
+ */
1801
+ private _refreshActivationCacheIfNeeded<Value>(
1802
+ binding: Binding<Value>,
1803
+ needsActivation: boolean,
1804
+ ): boolean {
1805
+ if (binding.kind === "class" && this._classHasPostConstruct.get(binding.target) === undefined) {
1806
+ this._refreshClassPostConstructCache(binding.target);
1807
+ this._activationNeedByBindingId.delete(binding.id);
1808
+ return this._needsActivation(binding);
1809
+ }
1810
+ return needsActivation;
1811
+ }
1812
+
1813
+ private _requiresResolutionContext<const Value>(binding: Binding<Value>): boolean {
1814
+ return binding.kind === "dynamic" || binding.kind === "dynamic-async";
1815
+ }
1816
+
1817
+ private _acquireSyncResolutionContext(
1818
+ resolutionPath: Array<string>,
1819
+ resolutionStack: Array<ResolutionFrame>,
1820
+ hint: ResolveOptions | undefined,
1821
+ ): DefaultResolutionContext {
1822
+ const depth = resolutionStack.length;
1823
+ const existing = this._syncResolutionContextPool[depth];
1824
+ if (existing !== undefined) {
1825
+ existing.reset(this as unknown as ResolverCallbacks, resolutionPath, resolutionStack, hint);
1826
+ return existing;
1827
+ }
1828
+ const created = new DefaultResolutionContext(
1829
+ this as unknown as ResolverCallbacks,
1830
+ resolutionPath,
1831
+ resolutionStack,
1832
+ hint,
1833
+ );
1834
+ this._syncResolutionContextPool[depth] = created;
1835
+ return created;
1836
+ }
1837
+ }