import-cycle-breaker.tsā¢7.2 kB
/**
 * Import Cycle Breaker Utility
 * 
 * Detects and breaks circular import dependencies by tracking import stack
 * and providing safe fallbacks when circular imports are detected.
 * 
 * This utility helps prevent infinite recursion during dynamic module loading
 * by maintaining a stack of currently importing modules and detecting cycles.
 */
import logger from '../logger.js';
/**
 * Import cycle detection and breaking utility
 */
export class ImportCycleBreaker {
  private static importStack = new Set<string>();
  private static importHistory = new Map<string, { timestamp: number; success: boolean }>();
  private static readonly IMPORT_TIMEOUT = 5000; // 5 seconds
  private static readonly HISTORY_CLEANUP_INTERVAL = 60000; // 1 minute
  private static cleanupTimer?: NodeJS.Timeout;
  /**
   * Safely import a module with circular dependency detection
   * 
   * @param modulePath - The path to the module to import
   * @param importName - The specific export to import from the module
   * @returns The imported module/export or null if circular dependency detected
   */
  static async safeImport<T>(modulePath: string, importName?: string): Promise<T | null> {
    const importKey = importName ? `${modulePath}:${importName}` : modulePath;
    
    // Check if this import is already in progress (circular dependency)
    if (this.importStack.has(importKey)) {
      logger.warn({ 
        modulePath, 
        importName, 
        currentStack: Array.from(this.importStack) 
      }, 'Circular import detected, using fallback');
      
      this.recordImportAttempt(importKey, false);
      return null;
    }
    // Check recent failed imports to avoid repeated failures
    const recentAttempt = this.importHistory.get(importKey);
    if (recentAttempt && !recentAttempt.success && 
        (Date.now() - recentAttempt.timestamp) < this.IMPORT_TIMEOUT) {
      logger.debug({ 
        modulePath, 
        importName, 
        lastAttempt: recentAttempt.timestamp 
      }, 'Skipping recent failed import attempt');
      return null;
    }
    // Add to import stack
    this.importStack.add(importKey);
    
    try {
      logger.debug({ modulePath, importName }, 'Starting safe import');
      
      // Set timeout for import operation
      const importPromise = this.performImport<T>(modulePath, importName);
      const timeoutPromise = new Promise<never>((_, reject) => {
        setTimeout(() => reject(new Error('Import timeout')), this.IMPORT_TIMEOUT);
      });
      
      const module = await Promise.race([importPromise, timeoutPromise]);
      
      logger.debug({ modulePath, importName }, 'Safe import completed successfully');
      this.recordImportAttempt(importKey, true);
      
      return module;
      
    } catch (error) {
      logger.warn({ 
        err: error, 
        modulePath, 
        importName 
      }, 'Safe import failed');
      
      this.recordImportAttempt(importKey, false);
      return null;
      
    } finally {
      // Remove from import stack
      this.importStack.delete(importKey);
      
      // Start cleanup timer if not already running
      this.startCleanupTimer();
    }
  }
  /**
   * Perform the actual module import
   */
  private static async performImport<T>(modulePath: string, importName?: string): Promise<T> {
    const module = await import(modulePath);
    
    if (importName) {
      if (!(importName in module)) {
        throw new Error(`Export '${importName}' not found in module '${modulePath}'`);
      }
      return module[importName];
    }
    
    return module;
  }
  /**
   * Record import attempt for history tracking
   */
  private static recordImportAttempt(importKey: string, success: boolean): void {
    this.importHistory.set(importKey, {
      timestamp: Date.now(),
      success
    });
  }
  /**
   * Start cleanup timer to remove old import history entries
   */
  private static startCleanupTimer(): void {
    if (this.cleanupTimer) {
      return;
    }
    
    this.cleanupTimer = setTimeout(() => {
      this.cleanupImportHistory();
      this.cleanupTimer = undefined;
    }, this.HISTORY_CLEANUP_INTERVAL);
  }
  /**
   * Clean up old import history entries
   */
  private static cleanupImportHistory(): void {
    const now = Date.now();
    const cutoff = now - (this.HISTORY_CLEANUP_INTERVAL * 2); // Keep 2 intervals worth
    
    let cleanedCount = 0;
    for (const [key, entry] of this.importHistory.entries()) {
      if (entry.timestamp < cutoff) {
        this.importHistory.delete(key);
        cleanedCount++;
      }
    }
    
    if (cleanedCount > 0) {
      logger.debug({ cleanedCount }, 'Cleaned up old import history entries');
    }
  }
  /**
   * Get current import stack for debugging
   */
  static getCurrentImportStack(): string[] {
    return Array.from(this.importStack);
  }
  /**
   * Get import history for debugging
   */
  static getImportHistory(): Record<string, { timestamp: number; success: boolean }> {
    const history: Record<string, { timestamp: number; success: boolean }> = {};
    for (const [key, value] of this.importHistory.entries()) {
      history[key] = value;
    }
    return history;
  }
  /**
   * Clear all import tracking (useful for testing)
   */
  static clearAll(): void {
    this.importStack.clear();
    this.importHistory.clear();
    if (this.cleanupTimer) {
      clearTimeout(this.cleanupTimer);
      this.cleanupTimer = undefined;
    }
  }
  /**
   * Check if a specific import is currently in progress
   */
  static isImportInProgress(modulePath: string, importName?: string): boolean {
    const importKey = importName ? `${modulePath}:${importName}` : modulePath;
    return this.importStack.has(importKey);
  }
  /**
   * Get statistics about import operations
   */
  static getStatistics(): {
    currentImports: number;
    historyEntries: number;
    successfulImports: number;
    failedImports: number;
  } {
    let successfulImports = 0;
    let failedImports = 0;
    
    for (const entry of this.importHistory.values()) {
      if (entry.success) {
        successfulImports++;
      } else {
        failedImports++;
      }
    }
    
    return {
      currentImports: this.importStack.size,
      historyEntries: this.importHistory.size,
      successfulImports,
      failedImports
    };
  }
  /**
   * Create a safe import wrapper for a specific module
   * Useful for creating module-specific import functions
   */
  static createModuleImporter(modulePath: string) {
    return async <T>(importName?: string): Promise<T | null> => {
      return this.safeImport<T>(modulePath, importName);
    };
  }
  /**
   * Batch import multiple modules safely
   * Returns results in the same order as input, with null for failed imports
   */
  static async safeBatchImport<T>(
    imports: Array<{ modulePath: string; importName?: string }>
  ): Promise<Array<T | null>> {
    const results = await Promise.allSettled(
      imports.map(({ modulePath, importName }) => 
        this.safeImport<T>(modulePath, importName)
      )
    );
    
    return results.map(result => 
      result.status === 'fulfilled' ? result.value : null
    );
  }
}