/** * Resolve a schedule interval in milliseconds. * * Priority: * 1. env var if set and valid * 2. fallbackMs default * * In development mode, enforces a minimum of 10s to avoid accidental API spam. */ export function resolveInterval( envVar: string, fallbackMs: number ): number { const raw = Number(process.env[envVar]); let interval = Number.isFinite(raw) && raw > 0 ? raw : fallbackMs; if (process.env.NODE_ENV === 'development' && interval < 10_000) { console.warn( `Sync interval from ${envVar} too low (${interval}ms), using 10s minimum` ); interval = 10_000; } return interval; } // --------------------------------------------------------------------------- // Scheduler state holders (mutable, per-scheduler) // --------------------------------------------------------------------------- export interface SchedulerState { running: boolean; timer: ReturnType | null; intervalMs: number | null; nextTickAt: number | null; } export function createSchedulerState(): SchedulerState { return { running: false, timer: null, intervalMs: null, nextTickAt: null }; } export function startScheduler( state: SchedulerState, intervalMs: number, task: () => Promise, label: string ): void { if (process.env.NODE_ENV === 'test') return; if (state.running) return; state.running = true; state.intervalMs = intervalMs; state.nextTickAt = Date.now() + intervalMs; state.timer = setInterval(() => { state.nextTickAt = Date.now() + (state.intervalMs ?? intervalMs); task().catch(err => console.error(`❌ ${label} scheduler run error:`, err) ); }, intervalMs); } export function stopScheduler(state: SchedulerState): void { state.running = false; if (state.timer) { clearInterval(state.timer); state.timer = null; } state.intervalMs = null; state.nextTickAt = null; } export function secondsUntilNextTick( state: SchedulerState, nowMs: number = Date.now() ): number | null { const next = state.nextTickAt; if (next == null) return null; const secs = (next - nowMs) / 1000; return secs > 0 ? secs : 0; }