// Background scheduler: runs `runBackupCycle` on an interval, with a simple // in-process mutex so concurrent triggers don't stack. // // The scheduler is deliberately minimal — a single `setInterval` is fine for // an app whose job is "check in periodically". If we ever want cron-style // "daily at 03:00" we can swap the trigger without changing `runBackupCycle`. import { env } from '../env'; import { runBackupCycle, type RunSummary } from './runner'; let running: Promise | null = null; let timer: ReturnType | null = null; let lastSummary: RunSummary | null = null; let lastError: Error | null = null; let lastRanAt: Date | null = null; export async function triggerBackup(): Promise { if (running) return running; running = (async () => { try { const summary = await runBackupCycle(); lastSummary = summary; lastError = null; return summary; } catch (err) { lastError = err as Error; throw err; } finally { lastRanAt = new Date(); running = null; } })(); return running; } export function startScheduler(): void { if (timer) return; const runOnce = () => { triggerBackup().catch((err) => { console.error('[scheduler] backup cycle failed:', err); }); }; // Fire once at startup, then on the interval. The in-process mutex stops // overlap if a cycle runs longer than the interval. runOnce(); timer = setInterval(runOnce, env.BACKUP_INTERVAL_MS); } export function stopScheduler(): void { if (timer) { clearInterval(timer); timer = null; } } export function schedulerState(): { isRunning: boolean; lastRanAt: Date | null; lastSummary: RunSummary | null; lastError: string | null; } { return { isRunning: running !== null, lastRanAt, lastSummary, lastError: lastError?.message ?? null }; }