UNPKG

kist

Version:

Lightweight Package Pipeline Processor with Plugin Architecture

291 lines 9.92 kB
import crypto from "crypto"; import fs from "fs"; import path from "path"; import { AbstractProcess } from "../abstract/AbstractProcess.js"; import { FileCache } from "./FileCache.js"; export class BuildCache extends AbstractProcess { static instance; cacheIndex = new Map(); cacheDir; maxCacheSize; ttl; indexPath; initialized = false; fileCache; stats = { hits: 0, misses: 0, stored: 0, restored: 0, evicted: 0, }; constructor(options = {}) { super(); this.cacheDir = options.cacheDir || path.join(process.cwd(), ".kist-cache", "build"); this.maxCacheSize = options.maxCacheSize || 1024 * 1024 * 1024; this.ttl = options.ttl || 7 * 24 * 60 * 60 * 1000; this.indexPath = path.join(this.cacheDir, "build-index.json"); this.fileCache = FileCache.getInstance(); } static getInstance(options) { if (!BuildCache.instance) { BuildCache.instance = new BuildCache(options); } return BuildCache.instance; } static resetInstance() { BuildCache.instance = undefined; } async initialize() { if (this.initialized) return; try { await fs.promises.mkdir(this.cacheDir, { recursive: true }); await this.loadIndex(); await this.fileCache.initialize(); this.initialized = true; this.logDebug(`BuildCache initialized with ${this.cacheIndex.size} entries.`); } catch (error) { this.logWarn(`Failed to initialize build cache: ${error}`); this.cacheIndex.clear(); this.initialized = true; } } async lookup(actionName, inputFiles, config = {}) { await this.initialize(); const cacheKey = await this.computeCacheKey(actionName, inputFiles, config); const entry = this.cacheIndex.get(cacheKey); if (!entry) { this.stats.misses++; return { found: false }; } if (Date.now() - entry.createdAt > this.ttl) { this.cacheIndex.delete(cacheKey); this.stats.misses++; return { found: false }; } const currentInputHash = await this.computeInputHash(inputFiles); if (currentInputHash !== entry.inputHash) { this.cacheIndex.delete(cacheKey); this.stats.misses++; return { found: false }; } const outputsExist = await this.verifyOutputFiles(entry.outputFiles); if (!outputsExist) { const restored = await this.restoreFromArtifacts(cacheKey, entry); if (restored) { this.stats.restored++; this.stats.hits++; return { found: true, outputFiles: entry.outputFiles, restored: true, }; } this.cacheIndex.delete(cacheKey); this.stats.misses++; return { found: false }; } this.stats.hits++; this.logDebug(`Cache hit for ${actionName}`); return { found: true, outputFiles: entry.outputFiles }; } async store(actionName, inputFiles, outputFiles, config = {}, buildDuration) { await this.initialize(); const cacheKey = await this.computeCacheKey(actionName, inputFiles, config); const inputHash = await this.computeInputHash(inputFiles); const configHash = this.computeConfigHash(config); const entry = { inputHash, outputFiles, configHash, createdAt: Date.now(), buildDuration, }; await this.storeArtifacts(cacheKey, outputFiles); this.cacheIndex.set(cacheKey, entry); this.stats.stored++; await this.fileCache.updateFileEntries(inputFiles); await this.cleanupIfNeeded(); this.logDebug(`Stored cache entry for ${actionName}`); } invalidateAction(actionName) { for (const [key] of this.cacheIndex) { if (key.startsWith(`${actionName}:`)) { this.cacheIndex.delete(key); } } } async clear() { this.cacheIndex.clear(); this.stats = { hits: 0, misses: 0, stored: 0, restored: 0, evicted: 0, }; try { await fs.promises.rm(this.cacheDir, { recursive: true, force: true, }); await fs.promises.mkdir(this.cacheDir, { recursive: true }); } catch { } this.logInfo("BuildCache cleared."); } async save() { try { const data = JSON.stringify(Object.fromEntries(this.cacheIndex), null, 2); await fs.promises.writeFile(this.indexPath, data, "utf-8"); this.logDebug("BuildCache index saved."); } catch (error) { this.logWarn(`Failed to save build cache index: ${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.cacheIndex.size, hitRate, }; } async computeCacheKey(actionName, inputFiles, config) { const sortedFiles = [...inputFiles].sort().join("|"); const configStr = JSON.stringify(config); const data = `${actionName}:${sortedFiles}:${configStr}`; return `${actionName}:${crypto.createHash("md5").update(data).digest("hex")}`; } async computeInputHash(inputFiles) { const hashes = []; for (const file of inputFiles.sort()) { try { const content = await fs.promises.readFile(file); hashes.push(crypto.createHash("sha256").update(content).digest("hex")); } catch { hashes.push("missing"); } } return crypto .createHash("sha256") .update(hashes.join(":")) .digest("hex"); } computeConfigHash(config) { return crypto .createHash("md5") .update(JSON.stringify(config)) .digest("hex"); } async verifyOutputFiles(outputFiles) { for (const file of outputFiles) { try { await fs.promises.access(file, fs.constants.R_OK); } catch { return false; } } return true; } async storeArtifacts(cacheKey, outputFiles) { const artifactDir = path.join(this.cacheDir, "artifacts", cacheKey); try { await fs.promises.mkdir(artifactDir, { recursive: true }); for (const file of outputFiles) { const artifactPath = path.join(artifactDir, path.basename(file)); await fs.promises.copyFile(file, artifactPath); } } catch (error) { this.logWarn(`Failed to store artifacts for ${cacheKey}: ${error}`); } } async restoreFromArtifacts(cacheKey, entry) { const artifactDir = path.join(this.cacheDir, "artifacts", cacheKey); try { for (const outputFile of entry.outputFiles) { const artifactPath = path.join(artifactDir, path.basename(outputFile)); const outputDir = path.dirname(outputFile); await fs.promises.mkdir(outputDir, { recursive: true }); await fs.promises.copyFile(artifactPath, outputFile); } return true; } catch { return false; } } async loadIndex() { try { const data = await fs.promises.readFile(this.indexPath, "utf-8"); const parsed = JSON.parse(data); this.cacheIndex = new Map(Object.entries(parsed)); } catch { this.cacheIndex = new Map(); } } async cleanupIfNeeded() { const artifactsPath = path.join(this.cacheDir, "artifacts"); try { const size = await this.getDirectorySize(artifactsPath); if (size > this.maxCacheSize) { const entries = Array.from(this.cacheIndex.entries()).sort(([, a], [, b]) => a.createdAt - b.createdAt); const toEvict = Math.ceil(entries.length * 0.2); for (let i = 0; i < toEvict; i++) { const [key] = entries[i]; await this.evictEntry(key); this.stats.evicted++; } } } catch { } } async evictEntry(cacheKey) { this.cacheIndex.delete(cacheKey); const artifactDir = path.join(this.cacheDir, "artifacts", cacheKey); try { await fs.promises.rm(artifactDir, { recursive: true, force: true, }); } catch { } } async getDirectorySize(dirPath) { let totalSize = 0; try { const entries = await fs.promises.readdir(dirPath, { withFileTypes: true, }); for (const entry of entries) { const fullPath = path.join(dirPath, entry.name); if (entry.isDirectory()) { totalSize += await this.getDirectorySize(fullPath); } else { const stat = await fs.promises.stat(fullPath); totalSize += stat.size; } } } catch { } return totalSize; } } //# sourceMappingURL=BuildCache.js.map