import { TIER_BASE_INTERVAL, TIER_MAX_INTERVAL, TIER_MIN_INTERVAL, type Tier } from "./taxonomy"; /** * Adaptive polling. The next interval depends on tier bounds, how recently the sensor * changed, how often it changes, error state and HTTP cache behaviour (304s are cheap * so we can afford to poll a bit faster). */ export interface ScheduleInput { tier: Tier; baseIntervalSeconds?: number | null; lastChangeAt?: Date | null; /** changes in the last 7 days */ changes7d: number; /** meaningful events in the last 7 days */ events7d: number; consecutiveErrors: number; /** last run served 304 Not Modified */ lastWas304: boolean; now?: Date; } export function nextIntervalSeconds(i: ScheduleInput): number { const now = i.now ?? new Date(); const base = i.baseIntervalSeconds ?? TIER_BASE_INTERVAL[i.tier]; const min = TIER_MIN_INTERVAL[i.tier]; const max = TIER_MAX_INTERVAL[i.tier]; let interval = base; if (i.lastChangeAt) { const ageMin = (now.getTime() - i.lastChangeAt.getTime()) / 60000; if (ageMin < 15) interval = base / 6; // burst window else if (ageMin < 60) interval = base / 3; else if (ageMin < 24 * 60) interval = base / 1.5; else if (ageMin > 14 * 24 * 60) interval = base * 3; else if (ageMin > 7 * 24 * 60) interval = base * 2; } else { interval = base * 1.5; } // Frequent changers get tighter polling; the weight is bounded so the tier still rules. const perDay = i.changes7d / 7; if (perDay >= 3) interval *= 0.6; else if (perDay >= 1) interval *= 0.8; if (i.events7d >= 3) interval *= 0.8; if (i.lastWas304) interval *= 0.85; if (i.consecutiveErrors > 0) interval = Math.max(interval, base) * Math.min(16, 2 ** i.consecutiveErrors); // ±10 % jitter to avoid thundering herds const jitter = 1 + (Math.random() * 0.2 - 0.1); return Math.round(Math.max(min, Math.min(max * (i.consecutiveErrors ? 4 : 1), interval * jitter))); }