kist
Version:
Lightweight Package Pipeline Processor with Plugin Architecture
206 lines • 7.63 kB
JavaScript
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