import { appendFile, mkdir, readFile, readdir, rename, rm, writeFile } from "node:fs/promises"; import type { Dirent } from "node:fs"; import { join } from "node:path"; import type { BaseEvent } from "../types.js"; function dayPath(eventsDir: string, date: Date): string { return join(eventsDir, String(date.getUTCFullYear()), String(date.getUTCMonth() + 1).padStart(2, "0"), String(date.getUTCDate()).padStart(2, "0")); } export class EventStore { constructor(private readonly eventsDir: string) {} async append(value: BaseEvent): Promise { const date = new Date(value.timestamp); const directory = dayPath(this.eventsDir, date); await mkdir(directory, { recursive: true, mode: 0o700 }); await appendFile(join(directory, `${value.runId}.jsonl`), `${JSON.stringify(value)}\n`, { encoding: "utf8", mode: 0o600 }); } async all(): Promise { const files: string[] = []; const walk = async (directory: string): Promise => { let entries: Dirent[]; try { entries = await readdir(directory, { withFileTypes: true }); } catch (error) { if ((error as NodeJS.ErrnoException).code === "ENOENT") return; throw error; } await Promise.all(entries.map(async (entry) => entry.isDirectory() ? walk(join(directory, entry.name)) : entry.name.endsWith(".jsonl") ? files.push(join(directory, entry.name)) : undefined)); }; await walk(this.eventsDir); const events = (await Promise.all(files.sort().map(async (file) => (await readFile(file, "utf8")).split("\n").filter(Boolean).map((line) => JSON.parse(line) as BaseEvent)))).flat(); return events.sort((a, b) => a.timestamp.localeCompare(b.timestamp)); } async prune(retentionDays: number, now = Date.now()): Promise { const cutoff = now - retentionDays * 86_400_000; const files: string[] = []; const walk = async (directory: string): Promise => { let entries: Dirent[]; try { entries = await readdir(directory, { withFileTypes: true }); } catch (error) { if ((error as NodeJS.ErrnoException).code === "ENOENT") return; throw error; } await Promise.all(entries.map(async (entry) => entry.isDirectory() ? walk(join(directory, entry.name)) : entry.name.endsWith(".jsonl") ? files.push(join(directory, entry.name)) : undefined)); }; await walk(this.eventsDir); let deleted = 0; for (const file of files) { const lines = (await readFile(file, "utf8")).split("\n").filter(Boolean); const kept = lines.filter((line) => { try { return Date.parse((JSON.parse(line) as BaseEvent).timestamp) >= cutoff; } catch { return true; } }); if (kept.length === lines.length) continue; if (!kept.length) await rm(file); else { const temporary = `${file}.${Date.now()}.tmp`; await writeFile(temporary, `${kept.join("\n")}\n`, { encoding: "utf8", mode: 0o600 }); await rename(temporary, file); } deleted += lines.length - kept.length; } return deleted; } }