UNPKG

kist

Version:

Lightweight Package Pipeline Processor with Plugin Architecture

310 lines 11.8 kB
import { __awaiter } from "tslib"; 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 { constructor(options = {}) { super(); this.cacheIndex = new Map(); this.initialized = false; this.stats = { hits: 0, misses: 0, stored: 0, restored: 0, evicted: 0, }; 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; } initialize() { return __awaiter(this, void 0, void 0, function* () { if (this.initialized) return; try { yield fs.promises.mkdir(this.cacheDir, { recursive: true }); yield this.loadIndex(); yield 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; } }); } lookup(actionName_1, inputFiles_1) { return __awaiter(this, arguments, void 0, function* (actionName, inputFiles, config = {}) { yield this.initialize(); const cacheKey = yield 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 = yield this.computeInputHash(inputFiles); if (currentInputHash !== entry.inputHash) { this.cacheIndex.delete(cacheKey); this.stats.misses++; return { found: false }; } const outputsExist = yield this.verifyOutputFiles(entry.outputFiles); if (!outputsExist) { const restored = yield 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 }; }); } store(actionName_1, inputFiles_1, outputFiles_1) { return __awaiter(this, arguments, void 0, function* (actionName, inputFiles, outputFiles, config = {}, buildDuration) { yield this.initialize(); const cacheKey = yield this.computeCacheKey(actionName, inputFiles, config); const inputHash = yield this.computeInputHash(inputFiles); const configHash = this.computeConfigHash(config); const entry = { inputHash, outputFiles, configHash, createdAt: Date.now(), buildDuration, }; yield this.storeArtifacts(cacheKey, outputFiles); this.cacheIndex.set(cacheKey, entry); this.stats.stored++; yield this.fileCache.updateFileEntries(inputFiles); yield 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); } } } clear() { return __awaiter(this, void 0, void 0, function* () { this.cacheIndex.clear(); this.stats = { hits: 0, misses: 0, stored: 0, restored: 0, evicted: 0, }; try { yield fs.promises.rm(this.cacheDir, { recursive: true, force: true, }); yield fs.promises.mkdir(this.cacheDir, { recursive: true }); } catch (_a) { } this.logInfo("BuildCache cleared."); }); } save() { return __awaiter(this, void 0, void 0, function* () { try { const data = JSON.stringify(Object.fromEntries(this.cacheIndex), null, 2); yield 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 Object.assign(Object.assign({}, this.stats), { size: this.cacheIndex.size, hitRate }); } computeCacheKey(actionName, inputFiles, config) { return __awaiter(this, void 0, void 0, function* () { const sortedFiles = [...inputFiles].sort().join("|"); const configStr = JSON.stringify(config); const data = `${actionName}:${sortedFiles}:${configStr}`; return `${actionName}:${crypto.createHash("md5").update(data).digest("hex")}`; }); } computeInputHash(inputFiles) { return __awaiter(this, void 0, void 0, function* () { const hashes = []; for (const file of inputFiles.sort()) { try { const content = yield fs.promises.readFile(file); hashes.push(crypto.createHash("sha256").update(content).digest("hex")); } catch (_a) { hashes.push("missing"); } } return crypto .createHash("sha256") .update(hashes.join(":")) .digest("hex"); }); } computeConfigHash(config) { return crypto .createHash("md5") .update(JSON.stringify(config)) .digest("hex"); } verifyOutputFiles(outputFiles) { return __awaiter(this, void 0, void 0, function* () { for (const file of outputFiles) { try { yield fs.promises.access(file, fs.constants.R_OK); } catch (_a) { return false; } } return true; }); } storeArtifacts(cacheKey, outputFiles) { return __awaiter(this, void 0, void 0, function* () { const artifactDir = path.join(this.cacheDir, "artifacts", cacheKey); try { yield fs.promises.mkdir(artifactDir, { recursive: true }); for (const file of outputFiles) { const artifactPath = path.join(artifactDir, path.basename(file)); yield fs.promises.copyFile(file, artifactPath); } } catch (error) { this.logWarn(`Failed to store artifacts for ${cacheKey}: ${error}`); } }); } restoreFromArtifacts(cacheKey, entry) { return __awaiter(this, void 0, void 0, function* () { 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); yield fs.promises.mkdir(outputDir, { recursive: true }); yield fs.promises.copyFile(artifactPath, outputFile); } return true; } catch (_a) { return false; } }); } loadIndex() { return __awaiter(this, void 0, void 0, function* () { try { const data = yield fs.promises.readFile(this.indexPath, "utf-8"); const parsed = JSON.parse(data); this.cacheIndex = new Map(Object.entries(parsed)); } catch (_a) { this.cacheIndex = new Map(); } }); } cleanupIfNeeded() { return __awaiter(this, void 0, void 0, function* () { const artifactsPath = path.join(this.cacheDir, "artifacts"); try { const size = yield 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]; yield this.evictEntry(key); this.stats.evicted++; } } } catch (_a) { } }); } evictEntry(cacheKey) { return __awaiter(this, void 0, void 0, function* () { this.cacheIndex.delete(cacheKey); const artifactDir = path.join(this.cacheDir, "artifacts", cacheKey); try { yield fs.promises.rm(artifactDir, { recursive: true, force: true, }); } catch (_a) { } }); } getDirectorySize(dirPath) { return __awaiter(this, void 0, void 0, function* () { let totalSize = 0; try { const entries = yield fs.promises.readdir(dirPath, { withFileTypes: true, }); for (const entry of entries) { const fullPath = path.join(dirPath, entry.name); if (entry.isDirectory()) { totalSize += yield this.getDirectorySize(fullPath); } else { const stat = yield fs.promises.stat(fullPath); totalSize += stat.size; } } } catch (_a) { } return totalSize; }); } } //# sourceMappingURL=BuildCache.js.map