UNPKG

kist

Version:

Lightweight Package Pipeline Processor with Plugin Architecture

192 lines 6.44 kB
import crypto from "crypto"; import fs from "fs"; import path from "path"; import { AbstractProcess } from "../abstract/AbstractProcess.js"; export class FileCache extends AbstractProcess { static instance; cache = new Map(); cacheDir; maxEntries; ttl; cacheIndexPath; initialized = false; stats = { hits: 0, misses: 0, evictions: 0, }; constructor(options = {}) { super(); this.cacheDir = options.cacheDir || path.join(process.cwd(), ".kist-cache"); this.maxEntries = options.maxEntries || 10000; this.ttl = options.ttl || 24 * 60 * 60 * 1000; this.cacheIndexPath = path.join(this.cacheDir, "file-cache.json"); } static getInstance(options) { if (!FileCache.instance) { FileCache.instance = new FileCache(options); } return FileCache.instance; } static resetInstance() { FileCache.instance = undefined; } async initialize() { if (this.initialized) return; try { await this.ensureCacheDirectory(); await this.loadCacheFromDisk(); this.initialized = true; this.logDebug(`FileCache initialized with ${this.cache.size} entries.`); } catch (error) { this.logWarn(`Failed to initialize file cache, starting fresh: ${error}`); this.cache.clear(); this.initialized = true; } } async hasFileChanged(filePath) { await this.initialize(); const normalizedPath = this.normalizePath(filePath); const entry = this.cache.get(normalizedPath); if (!entry) { this.stats.misses++; return true; } if (Date.now() - entry.cachedAt > this.ttl) { this.cache.delete(normalizedPath); this.stats.misses++; return true; } try { const stat = await fs.promises.stat(filePath); if (stat.mtimeMs === entry.mtime && stat.size === entry.size) { this.stats.hits++; return false; } const currentHash = await this.computeFileHash(filePath); if (currentHash === entry.hash) { entry.mtime = stat.mtimeMs; this.stats.hits++; return false; } this.stats.misses++; return true; } catch { this.cache.delete(normalizedPath); this.stats.misses++; return true; } } async updateFileEntry(filePath) { await this.initialize(); const normalizedPath = this.normalizePath(filePath); try { const stat = await fs.promises.stat(filePath); const hash = await this.computeFileHash(filePath); this.cache.set(normalizedPath, { hash, mtime: stat.mtimeMs, size: stat.size, cachedAt: Date.now(), }); if (this.cache.size > this.maxEntries) { await this.evictOldEntries(); } } catch (error) { this.logWarn(`Failed to update cache entry for ${filePath}: ${error}`); } } async getChangedFiles(filePaths) { const results = await Promise.all(filePaths.map(async (filePath) => ({ filePath, changed: await this.hasFileChanged(filePath), }))); return results.filter((r) => r.changed).map((r) => r.filePath); } async updateFileEntries(filePaths) { await Promise.all(filePaths.map((fp) => this.updateFileEntry(fp))); } invalidate(filePath) { const normalizedPath = this.normalizePath(filePath); this.cache.delete(normalizedPath); } invalidatePattern(pattern) { const regex = new RegExp(pattern.replace(/\*/g, ".*")); for (const key of this.cache.keys()) { if (regex.test(key)) { this.cache.delete(key); } } } clear() { this.cache.clear(); this.stats = { hits: 0, misses: 0, evictions: 0 }; this.logInfo("FileCache cleared."); } async save() { try { await this.ensureCacheDirectory(); const data = JSON.stringify(Object.fromEntries(this.cache), null, 2); await fs.promises.writeFile(this.cacheIndexPath, data, "utf-8"); this.logDebug(`FileCache saved with ${this.cache.size} entries.`); } catch (error) { this.logWarn(`Failed to save file cache to disk: ${error}`); } } getStats() { const total = this.stats.hits + this.stats.misses; const hitRate = total > 0 ? ((this.stats.hits / total) * 100).toFixed(2) + "%" : "N/A"; return { ...this.stats, size: this.cache.size, hitRate, }; } normalizePath(filePath) { return path.resolve(filePath); } async computeFileHash(filePath) { return new Promise((resolve, reject) => { const hash = crypto.createHash("sha256"); const stream = fs.createReadStream(filePath); stream.on("data", (chunk) => hash.update(chunk)); stream.on("end", () => resolve(hash.digest("hex"))); stream.on("error", reject); }); } async ensureCacheDirectory() { try { await fs.promises.mkdir(this.cacheDir, { recursive: true }); } catch { } } async loadCacheFromDisk() { try { const data = await fs.promises.readFile(this.cacheIndexPath, "utf-8"); const parsed = JSON.parse(data); this.cache = new Map(Object.entries(parsed)); } catch { this.cache = new Map(); } } async evictOldEntries() { const entriesToEvict = Math.floor(this.maxEntries * 0.1); const entries = Array.from(this.cache.entries()).sort(([, a], [, b]) => a.cachedAt - b.cachedAt); for (let i = 0; i < entriesToEvict && i < entries.length; i++) { this.cache.delete(entries[i][0]); this.stats.evictions++; } this.logDebug(`Evicted ${entriesToEvict} old cache entries.`); } } //# sourceMappingURL=FileCache.js.map