import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs" import { join } from "node:path" import { log } from "./logger" type JsonFileCacheStoreOptions = { getCacheDir: () => string filename: string logPrefix: string cacheLabel: string describe: (value: TValue) => Record serialize?: (value: TValue) => string } type JsonFileCacheStore = { read: () => TValue | null has: () => boolean write: (value: TValue) => void resetMemory: () => void } function toLogLabel(cacheLabel: string): string { return cacheLabel.toLowerCase() } export function createJsonFileCacheStore( options: JsonFileCacheStoreOptions, ): JsonFileCacheStore { let memoryValue: TValue | null | undefined let writtenInCurrentProcess = false function getCacheFilePath(): string { return join(options.getCacheDir(), options.filename) } function ensureCacheDir(): void { const cacheDir = options.getCacheDir() if (!existsSync(cacheDir)) { mkdirSync(cacheDir, { recursive: true }) } } function read(): TValue | null { if (memoryValue !== undefined) { return memoryValue } const cacheFile = getCacheFilePath() if (!existsSync(cacheFile)) { memoryValue = null log(`[${options.logPrefix}] ${options.cacheLabel} file not found`, { cacheFile }) return null } try { const content = readFileSync(cacheFile, "utf-8") const value = JSON.parse(content) as TValue memoryValue = value log(`[${options.logPrefix}] Read ${toLogLabel(options.cacheLabel)}`, options.describe(value)) return value } catch (error) { memoryValue = null log(`[${options.logPrefix}] Error reading ${toLogLabel(options.cacheLabel)}`, { error: String(error), }) return null } } function has(): boolean { // First check if we have a valid in-memory cache value // This handles sandbox environments where existsSync may fail across contexts if (memoryValue !== undefined && memoryValue !== null) { return true } // Check if we've written to this cache in the current process // This helps in sandbox environments where filesystem state may not persist across contexts if (writtenInCurrentProcess) { return true } // Fall back to filesystem check return existsSync(getCacheFilePath()) } function write(value: TValue): void { ensureCacheDir() const cacheFile = getCacheFilePath() try { writeFileSync(cacheFile, options.serialize?.(value) ?? JSON.stringify(value, null, 2)) memoryValue = value writtenInCurrentProcess = true log(`[${options.logPrefix}] ${options.cacheLabel} written`, options.describe(value)) } catch (error) { log(`[${options.logPrefix}] Error writing ${toLogLabel(options.cacheLabel)}`, { error: String(error), }) } } function resetMemory(): void { memoryValue = undefined writtenInCurrentProcess = false } return { read, has, write, resetMemory, } }