UNPKG

kist

Version:

Lightweight Package Pipeline Processor with Plugin Architecture

206 lines 7.63 kB
import { __awaiter } from "tslib"; import crypto from "crypto"; import fs from "fs"; import path from "path"; import { AbstractProcess } from "../abstract/AbstractProcess.js"; export class FileCache extends AbstractProcess { constructor(options = {}) { super(); this.cache = new Map(); this.initialized = false; this.stats = { hits: 0, misses: 0, evictions: 0, }; 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; } initialize() { return __awaiter(this, void 0, void 0, function* () { if (this.initialized) return; try { yield this.ensureCacheDirectory(); yield 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; } }); } hasFileChanged(filePath) { return __awaiter(this, void 0, void 0, function* () { yield 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 = yield fs.promises.stat(filePath); if (stat.mtimeMs === entry.mtime && stat.size === entry.size) { this.stats.hits++; return false; } const currentHash = yield this.computeFileHash(filePath); if (currentHash === entry.hash) { entry.mtime = stat.mtimeMs; this.stats.hits++; return false; } this.stats.misses++; return true; } catch (_a) { this.cache.delete(normalizedPath); this.stats.misses++; return true; } }); } updateFileEntry(filePath) { return __awaiter(this, void 0, void 0, function* () { yield this.initialize(); const normalizedPath = this.normalizePath(filePath); try { const stat = yield fs.promises.stat(filePath); const hash = yield this.computeFileHash(filePath); this.cache.set(normalizedPath, { hash, mtime: stat.mtimeMs, size: stat.size, cachedAt: Date.now(), }); if (this.cache.size > this.maxEntries) { yield this.evictOldEntries(); } } catch (error) { this.logWarn(`Failed to update cache entry for ${filePath}: ${error}`); } }); } getChangedFiles(filePaths) { return __awaiter(this, void 0, void 0, function* () { const results = yield Promise.all(filePaths.map((filePath) => __awaiter(this, void 0, void 0, function* () { return ({ filePath, changed: yield this.hasFileChanged(filePath), }); }))); return results.filter((r) => r.changed).map((r) => r.filePath); }); } updateFileEntries(filePaths) { return __awaiter(this, void 0, void 0, function* () { yield 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."); } save() { return __awaiter(this, void 0, void 0, function* () { try { yield this.ensureCacheDirectory(); const data = JSON.stringify(Object.fromEntries(this.cache), null, 2); yield 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 Object.assign(Object.assign({}, this.stats), { size: this.cache.size, hitRate }); } normalizePath(filePath) { return path.resolve(filePath); } computeFileHash(filePath) { return __awaiter(this, void 0, void 0, function* () { 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); }); }); } ensureCacheDirectory() { return __awaiter(this, void 0, void 0, function* () { try { yield fs.promises.mkdir(this.cacheDir, { recursive: true }); } catch (_a) { } }); } loadCacheFromDisk() { return __awaiter(this, void 0, void 0, function* () { try { const data = yield fs.promises.readFile(this.cacheIndexPath, "utf-8"); const parsed = JSON.parse(data); this.cache = new Map(Object.entries(parsed)); } catch (_a) { this.cache = new Map(); } }); } evictOldEntries() { return __awaiter(this, void 0, void 0, function* () { 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