192 lines
		
	
	
		
			5.6 KiB
		
	
	
	
		
			TypeScript
		
	
	
	
	
	
			
		
		
	
	
			192 lines
		
	
	
		
			5.6 KiB
		
	
	
	
		
			TypeScript
		
	
	
	
	
	
import * as plugins from '../plugins.js';
 | 
						|
import * as fs from 'fs';
 | 
						|
import type { IFileMetadata, IFileInfo } from './types.js';
 | 
						|
 | 
						|
/**
 | 
						|
 * LazyFileLoader handles efficient file loading by:
 | 
						|
 * - Scanning files for metadata without loading contents
 | 
						|
 * - Providing fast file size and token estimates
 | 
						|
 * - Loading contents only when requested
 | 
						|
 * - Parallel loading of selected files
 | 
						|
 */
 | 
						|
export class LazyFileLoader {
 | 
						|
  private projectRoot: string;
 | 
						|
  private metadataCache: Map<string, IFileMetadata> = new Map();
 | 
						|
 | 
						|
  /**
 | 
						|
   * Creates a new LazyFileLoader
 | 
						|
   * @param projectRoot - Root directory of the project
 | 
						|
   */
 | 
						|
  constructor(projectRoot: string) {
 | 
						|
    this.projectRoot = projectRoot;
 | 
						|
  }
 | 
						|
 | 
						|
  /**
 | 
						|
   * Scans files in given globs and creates metadata without loading contents
 | 
						|
   * @param globs - File patterns to scan (e.g., ['ts/**\/*.ts', 'test/**\/*.ts'])
 | 
						|
   * @returns Array of file metadata
 | 
						|
   */
 | 
						|
  public async scanFiles(globs: string[]): Promise<IFileMetadata[]> {
 | 
						|
    const metadata: IFileMetadata[] = [];
 | 
						|
 | 
						|
    for (const globPattern of globs) {
 | 
						|
      try {
 | 
						|
        const smartFiles = await plugins.smartfile.fs.fileTreeToObject(this.projectRoot, globPattern);
 | 
						|
        const fileArray = Array.isArray(smartFiles) ? smartFiles : [smartFiles];
 | 
						|
 | 
						|
        for (const smartFile of fileArray) {
 | 
						|
          try {
 | 
						|
            const meta = await this.getMetadata(smartFile.path);
 | 
						|
            metadata.push(meta);
 | 
						|
          } catch (error) {
 | 
						|
            // Skip files that can't be read
 | 
						|
            console.warn(`Failed to get metadata for ${smartFile.path}:`, error.message);
 | 
						|
          }
 | 
						|
        }
 | 
						|
      } catch (error) {
 | 
						|
        // Skip patterns that don't match any files
 | 
						|
        console.warn(`No files found for pattern ${globPattern}`);
 | 
						|
      }
 | 
						|
    }
 | 
						|
 | 
						|
    return metadata;
 | 
						|
  }
 | 
						|
 | 
						|
  /**
 | 
						|
   * Gets metadata for a single file without loading contents
 | 
						|
   * @param filePath - Absolute path to the file
 | 
						|
   * @returns File metadata
 | 
						|
   */
 | 
						|
  public async getMetadata(filePath: string): Promise<IFileMetadata> {
 | 
						|
    // Check cache first
 | 
						|
    if (this.metadataCache.has(filePath)) {
 | 
						|
      const cached = this.metadataCache.get(filePath)!;
 | 
						|
      const currentStats = await fs.promises.stat(filePath);
 | 
						|
 | 
						|
      // Return cached if file hasn't changed
 | 
						|
      if (cached.mtime === Math.floor(currentStats.mtimeMs)) {
 | 
						|
        return cached;
 | 
						|
      }
 | 
						|
    }
 | 
						|
 | 
						|
    // Get file stats
 | 
						|
    const stats = await fs.promises.stat(filePath);
 | 
						|
    const relativePath = plugins.path.relative(this.projectRoot, filePath);
 | 
						|
 | 
						|
    // Estimate tokens: rough estimate of ~4 characters per token
 | 
						|
    // This is faster than reading and tokenizing the entire file
 | 
						|
    const estimatedTokens = Math.ceil(stats.size / 4);
 | 
						|
 | 
						|
    const metadata: IFileMetadata = {
 | 
						|
      path: filePath,
 | 
						|
      relativePath,
 | 
						|
      size: stats.size,
 | 
						|
      mtime: Math.floor(stats.mtimeMs),
 | 
						|
      estimatedTokens,
 | 
						|
    };
 | 
						|
 | 
						|
    // Cache the metadata
 | 
						|
    this.metadataCache.set(filePath, metadata);
 | 
						|
 | 
						|
    return metadata;
 | 
						|
  }
 | 
						|
 | 
						|
  /**
 | 
						|
   * Loads file contents for selected files in parallel
 | 
						|
   * @param metadata - Array of file metadata to load
 | 
						|
   * @param tokenizer - Function to calculate accurate token count
 | 
						|
   * @returns Array of complete file info with contents
 | 
						|
   */
 | 
						|
  public async loadFiles(
 | 
						|
    metadata: IFileMetadata[],
 | 
						|
    tokenizer: (content: string) => number
 | 
						|
  ): Promise<IFileInfo[]> {
 | 
						|
    // Load files in parallel
 | 
						|
    const loadPromises = metadata.map(async (meta) => {
 | 
						|
      try {
 | 
						|
        const contents = await plugins.smartfile.fs.toStringSync(meta.path);
 | 
						|
        const tokenCount = tokenizer(contents);
 | 
						|
 | 
						|
        const fileInfo: IFileInfo = {
 | 
						|
          path: meta.path,
 | 
						|
          relativePath: meta.relativePath,
 | 
						|
          contents,
 | 
						|
          tokenCount,
 | 
						|
          importanceScore: meta.importanceScore,
 | 
						|
        };
 | 
						|
 | 
						|
        return fileInfo;
 | 
						|
      } catch (error) {
 | 
						|
        console.warn(`Failed to load file ${meta.path}:`, error.message);
 | 
						|
        return null;
 | 
						|
      }
 | 
						|
    });
 | 
						|
 | 
						|
    // Wait for all loads to complete and filter out failures
 | 
						|
    const results = await Promise.all(loadPromises);
 | 
						|
    return results.filter((r): r is IFileInfo => r !== null);
 | 
						|
  }
 | 
						|
 | 
						|
  /**
 | 
						|
   * Loads a single file with contents
 | 
						|
   * @param filePath - Absolute path to the file
 | 
						|
   * @param tokenizer - Function to calculate accurate token count
 | 
						|
   * @returns Complete file info with contents
 | 
						|
   */
 | 
						|
  public async loadFile(
 | 
						|
    filePath: string,
 | 
						|
    tokenizer: (content: string) => number
 | 
						|
  ): Promise<IFileInfo> {
 | 
						|
    const meta = await this.getMetadata(filePath);
 | 
						|
    const contents = await plugins.smartfile.fs.toStringSync(filePath);
 | 
						|
    const tokenCount = tokenizer(contents);
 | 
						|
    const relativePath = plugins.path.relative(this.projectRoot, filePath);
 | 
						|
 | 
						|
    return {
 | 
						|
      path: filePath,
 | 
						|
      relativePath,
 | 
						|
      contents,
 | 
						|
      tokenCount,
 | 
						|
      importanceScore: meta.importanceScore,
 | 
						|
    };
 | 
						|
  }
 | 
						|
 | 
						|
  /**
 | 
						|
   * Updates importance scores for metadata entries
 | 
						|
   * @param scores - Map of file paths to importance scores
 | 
						|
   */
 | 
						|
  public updateImportanceScores(scores: Map<string, number>): void {
 | 
						|
    for (const [path, score] of scores) {
 | 
						|
      const meta = this.metadataCache.get(path);
 | 
						|
      if (meta) {
 | 
						|
        meta.importanceScore = score;
 | 
						|
      }
 | 
						|
    }
 | 
						|
  }
 | 
						|
 | 
						|
  /**
 | 
						|
   * Clears the metadata cache
 | 
						|
   */
 | 
						|
  public clearCache(): void {
 | 
						|
    this.metadataCache.clear();
 | 
						|
  }
 | 
						|
 | 
						|
  /**
 | 
						|
   * Gets total estimated tokens for all cached metadata
 | 
						|
   */
 | 
						|
  public getTotalEstimatedTokens(): number {
 | 
						|
    let total = 0;
 | 
						|
    for (const meta of this.metadataCache.values()) {
 | 
						|
      total += meta.estimatedTokens;
 | 
						|
    }
 | 
						|
    return total;
 | 
						|
  }
 | 
						|
 | 
						|
  /**
 | 
						|
   * Gets cached metadata entries
 | 
						|
   */
 | 
						|
  public getCachedMetadata(): IFileMetadata[] {
 | 
						|
    return Array.from(this.metadataCache.values());
 | 
						|
  }
 | 
						|
}
 |