Project Files
src / utils / failedFileRegistry.ts
import * as fs from "fs/promises";
import * as path from "path";
interface FailedFileEntry {
fileHash: string;
reason: string;
timestamp: string;
}
/**
* Tracks files that failed indexing for a given hash so we can skip them
* when auto-reindexing unchanged data.
*/
export class FailedFileRegistry {
private loaded = false;
private entries: Record<string, FailedFileEntry> = {};
private queue: Promise<void> = Promise.resolve();
constructor(private readonly registryPath: string) {}
private async load(): Promise<void> {
if (this.loaded) {
return;
}
try {
const data = await fs.readFile(this.registryPath, "utf-8");
this.entries = JSON.parse(data) ?? {};
} catch {
this.entries = {};
}
this.loaded = true;
}
private async persist(): Promise<void> {
await fs.mkdir(path.dirname(this.registryPath), { recursive: true });
await fs.writeFile(this.registryPath, JSON.stringify(this.entries, null, 2), "utf-8");
}
private runExclusive<T>(operation: () => Promise<T>): Promise<T> {
const result = this.queue.then(operation);
this.queue = result.then(
() => {},
() => {},
);
return result;
}
async recordFailure(filePath: string, fileHash: string, reason: string): Promise<void> {
return this.runExclusive(async () => {
await this.load();
this.entries[filePath] = {
fileHash,
reason,
timestamp: new Date().toISOString(),
};
await this.persist();
});
}
async clearFailure(filePath: string): Promise<void> {
return this.runExclusive(async () => {
await this.load();
if (this.entries[filePath]) {
delete this.entries[filePath];
await this.persist();
}
});
}
async getFailureReason(filePath: string, fileHash: string): Promise<string | undefined> {
await this.load();
const entry = this.entries[filePath];
if (!entry) {
return undefined;
}
return entry.fileHash === fileHash ? entry.reason : undefined;
}
}