73 lines
2.5 KiB
TypeScript
73 lines
2.5 KiB
TypeScript
import fs from "node:fs/promises";
|
|
import path from "node:path";
|
|
|
|
/**
|
|
* Native runs remain replayable evidence. Instead of rewriting their raw result
|
|
* array, locally removed rows are recorded here and filtered at load time.
|
|
*/
|
|
export const NATIVE_SCANNER_RESULT_TOMBSTONES_FILE = "deleted-results.jsonl";
|
|
|
|
export interface NativeScannerResultTombstone {
|
|
version: 1;
|
|
resultId: string;
|
|
deletedAt: string;
|
|
imagePath?: string;
|
|
removeLinkedStoreRecord: boolean;
|
|
storeRecordId?: string;
|
|
}
|
|
|
|
export function nativeScannerResultTombstonePath(runDir: string) {
|
|
return path.join(runDir, NATIVE_SCANNER_RESULT_TOMBSTONES_FILE);
|
|
}
|
|
|
|
export async function loadNativeScannerResultTombstones(runDir: string): Promise<NativeScannerResultTombstone[]> {
|
|
try {
|
|
const raw = await fs.readFile(nativeScannerResultTombstonePath(runDir), "utf8");
|
|
return raw
|
|
.split(/\r?\n/)
|
|
.filter(Boolean)
|
|
.map(parseTombstone)
|
|
.filter((entry): entry is NativeScannerResultTombstone => Boolean(entry));
|
|
} catch (error) {
|
|
if (isMissingFileError(error)) return [];
|
|
throw error;
|
|
}
|
|
}
|
|
|
|
export async function loadNativeScannerDeletedResultIds(runDir: string) {
|
|
const tombstones = await loadNativeScannerResultTombstones(runDir);
|
|
return new Set(tombstones.map((entry) => entry.resultId));
|
|
}
|
|
|
|
export async function appendNativeScannerResultTombstone(
|
|
runDir: string,
|
|
tombstone: NativeScannerResultTombstone,
|
|
) {
|
|
const filePath = nativeScannerResultTombstonePath(runDir);
|
|
await fs.appendFile(filePath, `${JSON.stringify(tombstone)}\n`, "utf8");
|
|
return filePath;
|
|
}
|
|
|
|
function parseTombstone(value: string): NativeScannerResultTombstone | null {
|
|
try {
|
|
const parsed = JSON.parse(value) as Partial<NativeScannerResultTombstone>;
|
|
const resultId = typeof parsed.resultId === "string" ? parsed.resultId.trim() : "";
|
|
const deletedAt = typeof parsed.deletedAt === "string" ? parsed.deletedAt : "";
|
|
if (!resultId || !deletedAt) return null;
|
|
return {
|
|
version: 1,
|
|
resultId,
|
|
deletedAt,
|
|
...(typeof parsed.imagePath === "string" ? { imagePath: parsed.imagePath } : {}),
|
|
removeLinkedStoreRecord: Boolean(parsed.removeLinkedStoreRecord),
|
|
...(typeof parsed.storeRecordId === "string" ? { storeRecordId: parsed.storeRecordId } : {}),
|
|
};
|
|
} catch {
|
|
return null;
|
|
}
|
|
}
|
|
|
|
function isMissingFileError(error: unknown) {
|
|
return typeof error === "object" && error !== null && "code" in error && (error as { code?: string }).code === "ENOENT";
|
|
}
|