All files / src/internal/lifecycle circular-detector.mts

90% Statements 18/20
80% Branches 8/10
100% Functions 2/2
90% Lines 18/20

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 84 85 86 87 88 89 90 91 92 93 94            17x                                                             117x         117x 117x       117x 132x     132x 8x       124x     124x     124x 124x 1x       123x 16x 15x                 109x                   3x        
import type { InstanceHolder } from '../holder/instance-holder.mjs'
 
/**
 * Whether we're running in production mode.
 * In production, circular dependency detection is skipped for performance.
 */
const isProduction = process.env.NODE_ENV === 'production'
 
/**
 * Detects circular dependencies by analyzing the waitingFor relationships
 * between service holders.
 *
 * Uses BFS to traverse the waitingFor graph starting from a target holder
 * and checks if following the chain leads back to the waiter, indicating a circular dependency.
 *
 * Note: In production (NODE_ENV === 'production'), detection is skipped for performance.
 */
export class CircularDetector {
  /**
   * Detects if waiting for `targetName` from `waiterName` would create a cycle.
   *
   * This works by checking if `targetName` (or any holder in its waitingFor chain)
   * is currently waiting for `waiterName`. If so, waiting would create a deadlock.
   *
   * In production mode, this always returns null to skip the BFS traversal overhead.
   *
   * @param waiterName The name of the holder that wants to wait
   * @param targetName The name of the holder being waited on
   * @param getHolder Function to retrieve a holder by name
   * @returns The cycle path if a cycle is detected, null otherwise
   */
  static detectCycle(
    waiterName: string,
    targetName: string,
    getHolder: (name: string) => InstanceHolder | undefined,
  ): string[] | null {
    // Skip circular dependency detection in production for performance
    Iif (isProduction) {
      return null
    }
 
    // Use BFS to find if there's a path from targetName back to waiterName
    const visited = new Set<string>()
    const queue: Array<{ name: string; path: string[] }> = [
      { name: targetName, path: [waiterName, targetName] },
    ]
 
    while (queue.length > 0) {
      const { name: currentName, path } = queue.shift()!
 
      // If we've reached back to the waiter, we have a cycle
      if (currentName === waiterName) {
        return path
      }
 
      // Skip if already visited
      Iif (visited.has(currentName)) {
        continue
      }
      visited.add(currentName)
 
      // Get the holder and check what it's waiting for
      const holder = getHolder(currentName)
      if (!holder) {
        continue
      }
 
      // Add all services this holder is waiting for to the queue
      for (const waitingForName of holder.waitingFor) {
        if (!visited.has(waitingForName)) {
          queue.push({
            name: waitingForName,
            path: [...path, waitingForName],
          })
        }
      }
    }
 
    // No path found from target back to waiter, no cycle
    return null
  }
 
  /**
   * Formats a cycle path into a human-readable string.
   *
   * @param cycle The cycle path (array of service names)
   * @returns Formatted string like "ServiceA -> ServiceB -> ServiceA"
   */
  static formatCycle(cycle: string[]): string {
    return cycle.join(' -> ')
  }
}