import { powerMonitor, BrowserWindow } from 'electron' import { IPC } from '@shared/ipc' import type { Tick } from '@shared/types' import { isQuietAt } from '@shared/types' import { getExercises, getSettings, updateExercise } from './store' import { fireReminder } from './notifications' import { broadcastState } from './state-actions' /** * TICK_MS drives the per-second countdown UI; CHECK_MS gates the (cheaper) * "is anything due to fire?" pass so we don't iterate exercises every second. */ const TICK_MS = 1000 const CHECK_MS = 5000 let tickHandle: NodeJS.Timeout | null = null let powerListenersArmed = false let lastCheckAt = 0 let paused = false function checkDueExercises(): void { if (paused) return const settings = getSettings() if (!settings.globalEnabled) return // Inside the quiet window: defer all due fires until it closes. The next // CHECK_MS pass after the window ends will pick them up. if (isQuietAt(settings.quietHours, new Date())) return const now = Date.now() const exercises = getExercises() let anyFired = false for (const ex of exercises) { if (!ex.enabled) continue if (ex.nextFireAt <= now) { const updated = updateExercise(ex.id, { nextFireAt: now + ex.intervalMinutes * 60_000 }) if (updated) { anyFired = true fireReminder(updated, settings.notificationMode) } } } // Push fresh state so the renderer's Dashboard/Exercises pages don't show // stale `nextFireAt` until the next state-changing IPC arrives. if (anyFired) broadcastState() } function broadcastTicks(): void { const now = Date.now() const ticks: Tick[] = getExercises().map((e) => ({ exerciseId: e.id, msUntilFire: Math.max(0, e.nextFireAt - now), enabled: e.enabled })) for (const win of BrowserWindow.getAllWindows()) { if (!win.isDestroyed()) win.webContents.send(IPC.evtTick, ticks) } } function tick(): void { broadcastTicks() const now = Date.now() if (now - lastCheckAt >= CHECK_MS) { lastCheckAt = now checkDueExercises() } } export function startScheduler(): void { if (tickHandle) return lastCheckAt = 0 tickHandle = setInterval(tick, TICK_MS) // Run an immediate tick so renderer hydrates quickly. tick() // Only attach powerMonitor listeners once per process — startScheduler may // be invoked again after stopScheduler in dev hot-reload paths and we don't // want the same handler firing N times after a resume. if (!powerListenersArmed) { powerListenersArmed = true powerMonitor.on('resume', () => { lastCheckAt = 0 tick() }) powerMonitor.on('unlock-screen', () => { lastCheckAt = 0 tick() }) } } export function stopScheduler(): void { if (tickHandle) { clearInterval(tickHandle) tickHandle = null } } export function setPaused(value: boolean): void { paused = value } export function isPaused(): boolean { return paused } export function forceCheck(): void { lastCheckAt = 0 tick() }