All files / src/internal/context resolution-context.mts

100% Statements 7/7
100% Branches 2/2
100% Functions 4/4
100% Lines 7/7

Press n or j to go to the next uncovered block, b, p or k for the previous block.

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83                                                      18x     1425x 11x   1425x                                   484x                       928x                         13x            
import type { InstanceHolder } from '../holder/instance-holder.mjs'
import type { IAsyncLocalStorage } from './async-local-storage.types.mjs'
 
import { createAsyncLocalStorage } from './async-local-storage.mjs'
 
/**
 * Data stored in the resolution context during service instantiation.
 */
export interface ResolutionContextData {
  /** The holder that is currently being instantiated */
  waiterHolder: InstanceHolder
  /** Function to get a holder by name (for cycle detection) */
  getHolder: (name: string) => InstanceHolder | undefined
}
 
/**
 * AsyncLocalStorage for tracking the current resolution context.
 *
 * This allows tracking which service is being instantiated even across
 * async boundaries (like when inject() is called inside a constructor).
 * Essential for circular dependency detection.
 *
 * The actual implementation varies by environment:
 * - Production: No-op (returns undefined, run() just calls fn directly)
 * - Development: Real AsyncLocalStorage with full async tracking
 * - Browser: SyncLocalStorage for synchronous-only tracking
 */
let resolutionContext: IAsyncLocalStorage<ResolutionContextData> | null = null
 
function getResolutionContext(): IAsyncLocalStorage<ResolutionContextData> {
  if (!resolutionContext) {
    resolutionContext = createAsyncLocalStorage<ResolutionContextData>()
  }
  return resolutionContext
}
 
/**
 * Runs a function within a resolution context.
 *
 * The context tracks which holder is currently being instantiated,
 * allowing circular dependency detection to work correctly.
 *
 * @param waiterHolder The holder being instantiated
 * @param getHolder Function to retrieve holders by name
 * @param fn The function to run within the context
 */
export function withResolutionContext<T>(
  waiterHolder: InstanceHolder,
  getHolder: (name: string) => InstanceHolder | undefined,
  fn: () => T,
): T {
  return getResolutionContext().run({ waiterHolder, getHolder }, fn)
}
 
/**
 * Gets the current resolution context, if any.
 *
 * Returns undefined if we're not inside a resolution context
 * (e.g., when resolving a top-level service that has no parent).
 */
export function getCurrentResolutionContext():
  | ResolutionContextData
  | undefined {
  return getResolutionContext().getStore()
}
 
/**
 * Runs a function outside any resolution context.
 *
 * This is useful for async injections that should not participate
 * in circular dependency detection since they don't block.
 *
 * @param fn The function to run without resolution context
 */
export function withoutResolutionContext<T>(fn: () => T): T {
  // Run with undefined context to clear any current context
  return getResolutionContext().run(
    undefined as unknown as ResolutionContextData,
    fn,
  )
}