/ src / utils / fileReadCache.ts
fileReadCache.ts
 1  import { detectFileEncoding } from './file.js'
 2  import { getFsImplementation } from './fsOperations.js'
 3  
 4  type CachedFileData = {
 5    content: string
 6    encoding: BufferEncoding
 7    mtime: number
 8  }
 9  
10  /**
11   * A simple in-memory cache for file contents with automatic invalidation based on modification time.
12   * This eliminates redundant file reads in FileEditTool operations.
13   */
14  class FileReadCache {
15    private cache = new Map<string, CachedFileData>()
16    private readonly maxCacheSize = 1000
17  
18    /**
19     * Reads a file with caching. Returns both content and encoding.
20     * Cache key includes file path and modification time for automatic invalidation.
21     */
22    readFile(filePath: string): { content: string; encoding: BufferEncoding } {
23      const fs = getFsImplementation()
24  
25      // Get file stats for cache invalidation
26      let stats
27      try {
28        stats = fs.statSync(filePath)
29      } catch (error) {
30        // File was deleted, remove from cache and re-throw
31        this.cache.delete(filePath)
32        throw error
33      }
34  
35      const cacheKey = filePath
36      const cachedData = this.cache.get(cacheKey)
37  
38      // Check if we have valid cached data
39      if (cachedData && cachedData.mtime === stats.mtimeMs) {
40        return {
41          content: cachedData.content,
42          encoding: cachedData.encoding,
43        }
44      }
45  
46      // Cache miss or stale data - read the file
47      const encoding = detectFileEncoding(filePath)
48      const content = fs
49        .readFileSync(filePath, { encoding })
50        .replaceAll('\r\n', '\n')
51  
52      // Update cache
53      this.cache.set(cacheKey, {
54        content,
55        encoding,
56        mtime: stats.mtimeMs,
57      })
58  
59      // Evict oldest entries if cache is too large
60      if (this.cache.size > this.maxCacheSize) {
61        const firstKey = this.cache.keys().next().value
62        if (firstKey) {
63          this.cache.delete(firstKey)
64        }
65      }
66  
67      return { content, encoding }
68    }
69  
70    /**
71     * Clears the entire cache. Useful for testing or memory management.
72     */
73    clear(): void {
74      this.cache.clear()
75    }
76  
77    /**
78     * Removes a specific file from the cache.
79     */
80    invalidate(filePath: string): void {
81      this.cache.delete(filePath)
82    }
83  
84    /**
85     * Gets cache statistics for debugging/monitoring.
86     */
87    getStats(): { size: number; entries: string[] } {
88      return {
89        size: this.cache.size,
90        entries: Array.from(this.cache.keys()),
91      }
92    }
93  }
94  
95  // Export a singleton instance
96  export const fileReadCache = new FileReadCache()