import fs from "node:fs/promises"; import path from "node:path"; import type { ReviewSampleListResult, ReviewSamplesRepositoryPort, ReviewSamplePayload } from "./contracts.js"; import type { ReviewSampleRecord, SaveResultWithPath } from "../../src/types/global.js"; const SMALL_FILE_LIMIT_BYTES = 8 * 1024 * 1024; const TAIL_READ_LIMIT_BYTES = 32 * 1024 * 1024; export class ReviewSamplesRepository implements ReviewSamplesRepositoryPort { private readonly filePath: string; constructor(userDataPath: string, fileName = "review-samples.jsonl") { this.filePath = path.join(userDataPath, fileName); } async list(limit = 50): Promise { try { const safeLimit = Math.max(1, Math.min(200, Number(limit) || 50)); const stats = await fs.stat(this.filePath); const raw = stats.size <= SMALL_FILE_LIMIT_BYTES ? await fs.readFile(this.filePath, "utf8") : await readTailText(this.filePath, stats.size, TAIL_READ_LIMIT_BYTES); const lines = raw.split(/\r?\n/).filter(Boolean); const samples = lines .slice(-safeLimit) .map((line) => { try { return JSON.parse(line) as ReviewSampleRecord; } catch { return null; } }) .filter(Boolean) as ReviewSampleRecord[]; const total = stats.size <= SMALL_FILE_LIMIT_BYTES ? lines.length : Math.max(samples.length, lines.length); return { ok: true, samples: samples.reverse(), total, path: this.filePath }; } catch { return { ok: true, samples: [], total: 0, path: this.filePath }; } } async append(sample: ReviewSamplePayload): Promise { await fs.mkdir(path.dirname(this.filePath), { recursive: true }); await fs.appendFile(this.filePath, `${JSON.stringify({ savedAt: new Date().toISOString(), sample })}\n`, "utf8"); return { ok: true, path: this.filePath }; } } async function readTailText(filePath: string, fileSize: number, maxBytes: number) { const bytesToRead = Math.min(fileSize, maxBytes); const handle = await fs.open(filePath, "r"); try { const buffer = Buffer.alloc(bytesToRead); await handle.read(buffer, 0, bytesToRead, fileSize - bytesToRead); const text = buffer.toString("utf8"); const firstNewline = text.indexOf("\n"); return fileSize > bytesToRead && firstNewline >= 0 ? text.slice(firstNewline + 1) : text; } finally { await handle.close(); } }