import { checkService, nextState } from './probe.mjs'; export function createMonitor(store, config, { check = checkService, runScenario = null, now = Date.now, setTimer = setTimeout, clearTimer = clearTimeout } = {}) { let active = null; let stopped = true; let timer; let lastPruned = 0; let scenarioActive = null, scenarioTimer; function previousState(service, scope, current) { // Expected failures during maintenance do not count toward a new outage. if (current && store.inMaintenance(service, current.checked_at) && !store.inMaintenance(service, now()) && !store.hasOpenIncident(service, scope)) return null; return current; } async function cycle() { if (active) return active; active = (async () => { await Promise.all(config.services.map(async service => { const result = await check(service, config); store.record(service, result, nextState(previousState(service, 'availability', store.state(service)), result, config), now(), { notify: config.telegram?.enabled }); })); if (now() - lastPruned > 86400000) { store.prune(now() - config.retentionDays * 86400000); lastPruned = now(); } })(); try { await active; } finally { active = null; } } async function scenarioCycle() { if (!runScenario) return; if (scenarioActive) return scenarioActive; scenarioActive = (async () => { await Promise.all(config.services.filter(service => service.scenario).map(async service => { const previous = store.scenario(service); if (previous && now() - previous.checked_at < config.scenarioIntervalSeconds * 1000) return; const result = await runScenario(service); store.recordScenario(service, result, nextState(previousState(service, 'scenario', previous), result, { ...config, slowMs: config.scenarioSlowMs }), now(), { notify: config.telegram?.enabled }); })); })(); try { await scenarioActive; } finally { scenarioActive = null; } } async function loop() { try { await cycle(); } catch { console.error('Monitoring cycle failed; previous results will expire.'); } if (!stopped) timer = setTimer(loop, config.intervalSeconds * 1000); } async function scenarioLoop() { const interval = config.scenarioIntervalSeconds * 1000; let delay = interval; try { await scenarioCycle(); // Resume each saved deadline after a restart, even when a check is not due yet. for (const service of config.services.filter(service => service.scenario)) { const saved = store.scenario(service); if (saved) delay = Math.min(delay, Math.max(100, saved.checked_at + interval - now())); } } catch { console.error('User journey checks failed; previous results will expire.'); } if (!stopped) scenarioTimer = setTimer(scenarioLoop, delay); } return { cycle, scenarioCycle, start() { if (!stopped) return; stopped = false; void loop(); if (runScenario) void scenarioLoop(); }, async stop() { stopped = true; clearTimer(timer); clearTimer(scenarioTimer); await Promise.all([active, scenarioActive].filter(Boolean)); }, }; }