const MINUTE = 60_000; const HOUR = 60 * MINUTE; const DAY = 24 * HOUR; /** * Approximate period of a 5-field cron expression (minute hour day-of-month month day-of-week), * used for staleness checks ("no success within 2× the schedule interval"). Exact evaluation is * not needed: monthly → 31 d, weekly → 7 d, daily → 1 d (÷ number of listed hours), hourly → 1 h * (or N minutes for step expressions). Returns null for malformed expressions. */ export function scheduleIntervalMs(cron: string | undefined | null): number | null { if (!cron) return null; const f = cron.trim().split(/\s+/); if (f.length !== 5) return null; const [minute, hour, dom, month, dow] = f as [string, string, string, string, string]; if (month !== '*' || dom !== '*') return 31 * DAY; if (dow !== '*') return (7 * DAY) / Math.max(1, listCount(dow)); if (hour !== '*') { const step = stepOf(hour); if (step) return step * HOUR; return DAY / Math.max(1, listCount(hour)); } if (minute !== '*') { const step = stepOf(minute); if (step) return step * MINUTE; return HOUR / Math.max(1, listCount(minute)); } return MINUTE; } function listCount(field: string): number { return field.split(',').reduce((n, part) => { const m = /^(\d+)-(\d+)$/.exec(part); return n + (m ? Math.max(1, Number(m[2]) - Number(m[1]) + 1) : 1); }, 0); } function stepOf(field: string): number | null { const m = /^\*\/(\d+)$/.exec(field); return m ? Math.max(1, Number(m[1])) : null; } /** Stale = no success for more than `factor` × the schedule interval (default 2×). */ export function isStale(lastSuccessAt: Date | string | null | undefined, cron: string | undefined | null, now = Date.now(), factor = 2): { stale: boolean; ageMs: number | null; limitMs: number | null } { const limitMs = scheduleIntervalMs(cron); const ageMs = lastSuccessAt ? now - new Date(lastSuccessAt).getTime() : null; if (limitMs === null) return { stale: false, ageMs, limitMs: null }; if (ageMs === null) return { stale: true, ageMs: null, limitMs: limitMs * factor }; return { stale: ageMs > limitMs * factor, ageMs, limitMs: limitMs * factor }; } /** Human duration ("3 d 4 h", "12 min"). */ export function humanDuration(ms: number | null | undefined): string { if (ms === null || ms === undefined || !Number.isFinite(ms)) return '-'; const s = Math.round(ms / 1000); if (s < 60) return `${s} s`; const m = Math.floor(s / 60); if (m < 60) return `${m} min`; const h = Math.floor(m / 60); if (h < 48) return `${h} h ${m % 60} min`; const d = Math.floor(h / 24); return `${d} d ${h % 24} h`; }