src / health.ts
import * as fs from "node:fs";
import * as path from "node:path";
import type { EngineName } from "./config";
import type { PageKind } from "./parse";
// Per-engine counters. No queries or URLs are stored here.
export interface EngineHealth {
okCount: number;
failCount: number;
captchaCount: number;
consecutiveFailures: number;
lastOk?: string;
lastFailure?: string;
lastFailureKind?: string;
lastCaptcha?: string;
}
type HealthFile = Partial<Record<EngineName, EngineHealth>>;
export class HealthStore {
private file: string;
private data: HealthFile;
constructor(dataDir: string) {
this.file = path.join(dataDir, "health.json");
try {
this.data = JSON.parse(fs.readFileSync(this.file, "utf8")) as HealthFile;
} catch {
this.data = {};
}
}
get(engine: EngineName): EngineHealth {
return this.data[engine] ?? { okCount: 0, failCount: 0, captchaCount: 0, consecutiveFailures: 0 };
}
record(engine: EngineName, kind: PageKind | "error"): void {
const h = this.get(engine);
const now = new Date().toISOString();
if (kind === "ok" || kind === "no_results") {
h.okCount++;
h.consecutiveFailures = 0;
h.lastOk = now;
} else if (kind === "captcha") {
h.captchaCount++;
h.lastCaptcha = now;
} else {
h.failCount++;
h.consecutiveFailures++;
h.lastFailure = now;
h.lastFailureKind = kind;
}
this.data[engine] = h;
try {
fs.mkdirSync(path.dirname(this.file), { recursive: true });
fs.writeFileSync(this.file, JSON.stringify(this.data, null, 2), "utf8");
} catch {
// health tracking is best effort
}
}
describe(engine: EngineName): string {
const h = this.get(engine);
if (!h.okCount && !h.failCount && !h.captchaCount) return "no searches recorded yet";
const parts = [`${h.okCount} ok, ${h.failCount} failed, ${h.captchaCount} captcha`];
if (h.consecutiveFailures) {
parts.push(`failing ${h.consecutiveFailures} time(s) in a row (last: ${h.lastFailureKind} at ${h.lastFailure})`);
} else if (h.lastOk) {
parts.push(`last ok ${h.lastOk}`);
}
return parts.join("; ");
}
}
src / health.ts
import * as fs from "node:fs";
import * as path from "node:path";
import type { EngineName } from "./config";
import type { PageKind } from "./parse";
// Per-engine counters. No queries or URLs are stored here.
export interface EngineHealth {
okCount: number;
failCount: number;
captchaCount: number;
consecutiveFailures: number;
lastOk?: string;
lastFailure?: string;
lastFailureKind?: string;
lastCaptcha?: string;
}
type HealthFile = Partial<Record<EngineName, EngineHealth>>;
export class HealthStore {
private file: string;
private data: HealthFile;
constructor(dataDir: string) {
this.file = path.join(dataDir, "health.json");
try {
this.data = JSON.parse(fs.readFileSync(this.file, "utf8")) as HealthFile;
} catch {
this.data = {};
}
}
get(engine: EngineName): EngineHealth {
return this.data[engine] ?? { okCount: 0, failCount: 0, captchaCount: 0, consecutiveFailures: 0 };
}
record(engine: EngineName, kind: PageKind | "error"): void {
const h = this.get(engine);
const now = new Date().toISOString();
if (kind === "ok" || kind === "no_results") {
h.okCount++;
h.consecutiveFailures = 0;
h.lastOk = now;
} else if (kind === "captcha") {
h.captchaCount++;
h.lastCaptcha = now;
} else {
h.failCount++;
h.consecutiveFailures++;
h.lastFailure = now;
h.lastFailureKind = kind;
}
this.data[engine] = h;
try {
fs.mkdirSync(path.dirname(this.file), { recursive: true });
fs.writeFileSync(this.file, JSON.stringify(this.data, null, 2), "utf8");
} catch {
// health tracking is best effort
}
}
describe(engine: EngineName): string {
const h = this.get(engine);
if (!h.okCount && !h.failCount && !h.captchaCount) return "no searches recorded yet";
const parts = [`${h.okCount} ok, ${h.failCount} failed, ${h.captchaCount} captcha`];
if (h.consecutiveFailures) {
parts.push(`failing ${h.consecutiveFailures} time(s) in a row (last: ${h.lastFailureKind} at ${h.lastFailure})`);
} else if (h.lastOk) {
parts.push(`last ok ${h.lastOk}`);
}
return parts.join("; ");
}
}