Files
workinf_Blender_Wasm/tools/web/wbv2-failure-ledger.mjs
mes123456 b28b365f3a
Some checks failed
M6 deployable RC / quick (push) Has been cancelled
M6 deployable RC / chromium (push) Has been cancelled
M6 deployable RC / release (push) Has been cancelled
perf(web): add idempotent batch verification fast paths
2026-09-03 20:29:51 +08:00

102 lines
3.8 KiB
JavaScript

import crypto from "node:crypto";
import fs from "node:fs";
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")); }
catch { /* First failure or an interrupted write. */ }
const key = keyFor(entry);
if (ledger.entries.some((candidate) => candidate.key === key)) return false;
ledger.entries = [...ledger.entries, { ...entry, key, recordedAt: new Date().toISOString() }].slice(-200);
fs.mkdirSync(path.dirname(file), { recursive: true });
const temporary = `${file}.tmp-${process.pid}`;
fs.writeFileSync(temporary, `${JSON.stringify(ledger, null, 2)}\n`);
fs.renameSync(temporary, file);
return true;
}