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