perf(web): add idempotent batch verification fast paths
This commit is contained in:
@@ -5,7 +5,87 @@ import path from "node:path";
|
||||
const ledgerPath = (root) => path.join(root, "tests/golden/WBV2/failure-ledger.json");
|
||||
const keyFor = (entry) => crypto.createHash("sha256").update(JSON.stringify({ task: entry.task, phase: entry.phase, reason: entry.reason, childExit: entry.childExit ?? null })).digest("hex");
|
||||
|
||||
const readLedger = (root) => {
|
||||
try {
|
||||
const value = JSON.parse(fs.readFileSync(ledgerPath(root), "utf8"));
|
||||
return {
|
||||
schemaVersion: 2,
|
||||
entries: Array.isArray(value.entries) ? value.entries : [],
|
||||
hydratedLogs: Array.isArray(value.hydratedLogs) ? value.hydratedLogs : [],
|
||||
};
|
||||
}
|
||||
catch {
|
||||
return { schemaVersion: 2, entries: [], hydratedLogs: [] };
|
||||
}
|
||||
};
|
||||
|
||||
export function createFailureLedgerSession(root, { logPaths = [] } = {}) {
|
||||
const ledger = readLedger(root);
|
||||
const existing = new Set(ledger.entries.map((entry) => entry.key).filter(Boolean));
|
||||
const hydrated = new Set(ledger.hydratedLogs);
|
||||
let dirty = false;
|
||||
|
||||
// Recovery is deliberately narrow: only explicit cycle.completed events can
|
||||
// add entries, and each log is imported at most once.
|
||||
for (const relative of logPaths) {
|
||||
if (hydrated.has(relative)) continue;
|
||||
const file = path.join(root, relative);
|
||||
if (!fs.existsSync(file)) continue;
|
||||
let source;
|
||||
try { source = fs.readFileSync(file, "utf8"); }
|
||||
catch { continue; }
|
||||
if (!/cycle\.completed/u.test(source)) continue;
|
||||
for (const line of source.split("\n")) {
|
||||
if (!/cycle\.completed/u.test(line)) continue;
|
||||
let event;
|
||||
try {
|
||||
const start = line.indexOf("{");
|
||||
event = JSON.parse(start >= 0 ? line.slice(start) : line);
|
||||
}
|
||||
catch { event = null; }
|
||||
const entry = event?.failure ?? event?.entry;
|
||||
if (!entry || typeof entry !== "object" || !entry.task || !entry.phase || !entry.reason) continue;
|
||||
const key = keyFor(entry);
|
||||
if (existing.has(key)) continue;
|
||||
ledger.entries.push({ ...entry, key, recordedAt: event.recordedAt ?? new Date().toISOString(), hydratedFrom: relative });
|
||||
existing.add(key);
|
||||
dirty = true;
|
||||
}
|
||||
hydrated.add(relative);
|
||||
dirty = true;
|
||||
}
|
||||
|
||||
const record = (entry) => {
|
||||
const key = keyFor(entry);
|
||||
if (existing.has(key)) return false;
|
||||
ledger.entries.push({ ...entry, key, recordedAt: new Date().toISOString() });
|
||||
ledger.entries = ledger.entries.slice(-200);
|
||||
existing.add(key);
|
||||
dirty = true;
|
||||
return true;
|
||||
};
|
||||
const flush = () => {
|
||||
if (!dirty) return false;
|
||||
const file = ledgerPath(root);
|
||||
fs.mkdirSync(path.dirname(file), { recursive: true });
|
||||
const temporary = `${file}.tmp-${process.pid}`;
|
||||
fs.writeFileSync(temporary, `${JSON.stringify({ ...ledger, hydratedLogs: [...hydrated] }, null, 2)}\n`);
|
||||
fs.renameSync(temporary, file);
|
||||
dirty = false;
|
||||
return true;
|
||||
};
|
||||
return { record, flush, ledger };
|
||||
}
|
||||
|
||||
export function recordFailure(root, entry) {
|
||||
const session = createFailureLedgerSession(root);
|
||||
const recorded = session.record(entry);
|
||||
session.flush();
|
||||
return recorded;
|
||||
}
|
||||
|
||||
/* Legacy callers should migrate to createFailureLedgerSession so a cycle only writes once. */
|
||||
export function _legacyRecordFailure(root, entry) {
|
||||
const file = ledgerPath(root);
|
||||
let ledger = { schemaVersion: 1, entries: [] };
|
||||
try { ledger = JSON.parse(fs.readFileSync(file, "utf8")); }
|
||||
|
||||
Reference in New Issue
Block a user