const crypto = require("crypto"); const fs = require("fs"); const path = require("path"); class ToolCache { constructor(options = {}) { this.directory = options.directory; this.now = options.now || Date.now; } get(key, ttlSeconds) { if (!ttlSeconds) return null; const file = this.fileFor(key); try { const value = JSON.parse(fs.readFileSync(file, "utf8")); if (!Number.isFinite(value.created_at) || value.created_at + ttlSeconds * 1000 <= this.now()) { fs.rmSync(file, { force: true }); return null; } return value.data; } catch { return null; } } set(key, data) { fs.mkdirSync(this.directory, { recursive: true }); const file = this.fileFor(key); const temporary = `${file}.${process.pid}.tmp`; fs.writeFileSync(temporary, `${JSON.stringify({ created_at: this.now(), data })}\n`); try { fs.renameSync(temporary, file); } catch (error) { if (!["EEXIST", "EPERM"].includes(error.code)) throw error; fs.rmSync(file, { force: true }); fs.renameSync(temporary, file); } this.prune(200); return data; } clear() { fs.rmSync(this.directory, { recursive: true, force: true }); fs.mkdirSync(this.directory, { recursive: true }); } stats() { try { const files = fs.readdirSync(this.directory).filter((name) => name.endsWith(".json")); return { entries: files.length, bytes: files.reduce((total, name) => total + fs.statSync(path.join(this.directory, name)).size, 0) }; } catch { return { entries: 0, bytes: 0 }; } } prune(maximum) { let files; try { files = fs.readdirSync(this.directory) .filter((name) => name.endsWith(".json")) .map((name) => ({ name, mtime: fs.statSync(path.join(this.directory, name)).mtimeMs })) .sort((left, right) => right.mtime - left.mtime); } catch { return; } for (const file of files.slice(maximum)) fs.rmSync(path.join(this.directory, file.name), { force: true }); } fileFor(key) { return path.join(this.directory, `${crypto.createHash("sha256").update(String(key)).digest("hex")}.json`); } } module.exports = { ToolCache };