// ── Timer Engine ─────────────────────────────────────────────── // Core timer types, state machine, and lifecycle management import { v4 as uuidv4 } from 'uuid'; import type { UrgencyLevel } from './urgency'; import type { CascadeConfig, CascadeWarning } from './cascade'; import { calculateCascadeWarnings, getCascadeIntervals } from './cascade'; // ── Types ────────────────────────────────────────────────────── export type TimerType = 'alarm' | 'countdown' | 'pomodoro' | 'event'; export type TimerState = | 'idle' | 'active' | 'warning' | 'firing' | 'snoozed' | 'dismissed' | 'completed' | 'paused'; export interface Timer { id: string; type: TimerType; label: string; description?: string; urgency: UrgencyLevel; state: TimerState; // Time fields targetTime: number; // epoch ms — when the timer fires duration: number | null; // ms — for countdowns/pomodoro createdAt: number; startedAt: number | null; pausedAt: number | null; firedAt: number | null; dismissedAt: number | null; completedAt: number | null; // Elapsed tracking for pause/resume elapsedBeforePause: number; // ms accumulated before last pause // Cascade cascade: CascadeConfig; warnings: CascadeWarning[]; // Pomodoro-specific pomodoroConfig?: PomodoroConfig; pomodoroState?: PomodoroState; // Snooze snoozeCount: number; snoozedUntil: number | null; // Metadata category?: string; tags?: string[]; linkedTimerId?: string | null; recurringTimerId?: string | null; } export interface PomodoroConfig { workMinutes: number; breakMinutes: number; longBreakMinutes: number; rounds: number; } export interface PomodoroState { currentRound: number; isBreak: boolean; isLongBreak: boolean; completedRounds: number; } export const DEFAULT_POMODORO_CONFIG: PomodoroConfig = { workMinutes: 25, breakMinutes: 5, longBreakMinutes: 15, rounds: 4, }; // ── Factory Functions ────────────────────────────────────────── export interface CreateAlarmParams { label: string; targetTime: number; urgency?: UrgencyLevel; cascade?: CascadeConfig; category?: string; description?: string; } export function createAlarm(params: CreateAlarmParams): Timer { const cascade = params.cascade ?? { preset: 'standard', intervals: [] }; const intervals = getCascadeIntervals(cascade); const now = Date.now(); return { id: uuidv4(), type: 'alarm', label: params.label, description: params.description, urgency: params.urgency ?? 'standard', state: 'active', targetTime: params.targetTime, duration: params.targetTime - now, createdAt: now, startedAt: now, pausedAt: null, firedAt: null, dismissedAt: null, completedAt: null, elapsedBeforePause: 0, cascade, warnings: calculateCascadeWarnings(params.targetTime, intervals, now), snoozeCount: 0, snoozedUntil: null, category: params.category, }; } export interface CreateCountdownParams { label: string; durationMs: number; urgency?: UrgencyLevel; cascade?: CascadeConfig; category?: string; description?: string; } export function createCountdown(params: CreateCountdownParams): Timer { const now = Date.now(); const targetTime = now + params.durationMs; const cascade = params.cascade ?? { preset: 'standard', intervals: [] }; const intervals = getCascadeIntervals(cascade); return { id: uuidv4(), type: 'countdown', label: params.label, description: params.description, urgency: params.urgency ?? 'standard', state: 'active', targetTime, duration: params.durationMs, createdAt: now, startedAt: now, pausedAt: null, firedAt: null, dismissedAt: null, completedAt: null, elapsedBeforePause: 0, cascade, warnings: calculateCascadeWarnings(targetTime, intervals, now), snoozeCount: 0, snoozedUntil: null, category: params.category, }; } export interface CreateEventParams { label: string; targetTime: number; // epoch ms — the future event date urgency?: UrgencyLevel; cascade?: CascadeConfig; category?: string; description?: string; milestones?: number[]; // days before event to warn (e.g. [30, 7, 1]) } export const DEFAULT_EVENT_MILESTONES = [30, 7, 3, 1]; // days before export function createEvent(params: CreateEventParams): Timer { const now = Date.now(); const milestones = params.milestones ?? DEFAULT_EVENT_MILESTONES; // Convert milestone days to cascade warning minutes const milestoneMinutes = milestones .map((days) => days * 24 * 60) .filter((mins) => mins * 60_000 < (params.targetTime - now)); // only include if before now const cascade = params.cascade ?? { preset: 'custom', intervals: [] }; return { id: uuidv4(), type: 'event', label: params.label, description: params.description, urgency: params.urgency ?? 'gentle', state: 'active', targetTime: params.targetTime, duration: params.targetTime - now, createdAt: now, startedAt: now, pausedAt: null, firedAt: null, dismissedAt: null, completedAt: null, elapsedBeforePause: 0, cascade, warnings: calculateCascadeWarnings(params.targetTime, milestoneMinutes, now), snoozeCount: 0, snoozedUntil: null, category: params.category, }; } export interface CreatePomodoroParams { label?: string; config?: Partial; urgency?: UrgencyLevel; } export function createPomodoro(params: CreatePomodoroParams = {}): Timer { const now = Date.now(); const config = { ...DEFAULT_POMODORO_CONFIG, ...params.config }; const durationMs = config.workMinutes * 60 * 1000; const targetTime = now + durationMs; return { id: uuidv4(), type: 'pomodoro', label: params.label ?? 'Focus Session', urgency: params.urgency ?? 'standard', state: 'active', targetTime, duration: durationMs, createdAt: now, startedAt: now, pausedAt: null, firedAt: null, dismissedAt: null, completedAt: null, elapsedBeforePause: 0, cascade: { preset: 'minimal', intervals: [] }, warnings: calculateCascadeWarnings(targetTime, [1], now), pomodoroConfig: config, pomodoroState: { currentRound: 1, isBreak: false, isLongBreak: false, completedRounds: 0, }, snoozeCount: 0, snoozedUntil: null, }; } // ── State Transitions ────────────────────────────────────────── export function pauseTimer(timer: Timer): Timer { if (timer.state !== 'active' && timer.state !== 'warning') return timer; const now = Date.now(); const elapsed = timer.elapsedBeforePause + (now - (timer.startedAt ?? now)); return { ...timer, state: 'paused', pausedAt: now, elapsedBeforePause: elapsed, }; } export function resumeTimer(timer: Timer): Timer { if (timer.state !== 'paused') return timer; const now = Date.now(); const remainingMs = (timer.duration ?? 0) - timer.elapsedBeforePause; const newTargetTime = now + remainingMs; const intervals = getCascadeIntervals(timer.cascade); return { ...timer, state: 'active', startedAt: now, pausedAt: null, targetTime: newTargetTime, warnings: calculateCascadeWarnings(newTargetTime, intervals, now), }; } export function fireTimer(timer: Timer): Timer { if (timer.state === 'dismissed' || timer.state === 'completed') return timer; return { ...timer, state: 'firing', firedAt: Date.now(), }; } export function snoozeTimer(timer: Timer, snoozeMinutes: number): Timer { if (timer.state !== 'firing') return timer; const now = Date.now(); const snoozeUntil = now + snoozeMinutes * 60 * 1000; const intervals = getCascadeIntervals(timer.cascade); return { ...timer, state: 'snoozed', targetTime: snoozeUntil, snoozedUntil: snoozeUntil, snoozeCount: timer.snoozeCount + 1, warnings: calculateCascadeWarnings(snoozeUntil, intervals.filter(m => m <= snoozeMinutes), now), }; } export function dismissTimer(timer: Timer): Timer { return { ...timer, state: 'dismissed', dismissedAt: Date.now(), }; } export function completeTimer(timer: Timer): Timer { return { ...timer, state: 'completed', completedAt: Date.now(), }; } // ── Pomodoro Transitions ─────────────────────────────────────── export function advancePomodoro(timer: Timer): Timer | null { if (timer.type !== 'pomodoro' || !timer.pomodoroConfig || !timer.pomodoroState) return null; const { pomodoroConfig: config, pomodoroState: state } = timer; const now = Date.now(); if (state.isBreak || state.isLongBreak) { // Long break finished → all done if (state.isLongBreak) { return completeTimer(timer); } // Short break finished → start next work round const nextRound = state.currentRound + 1; if (nextRound > config.rounds) { return completeTimer(timer); } const durationMs = config.workMinutes * 60 * 1000; return { ...timer, state: 'active', targetTime: now + durationMs, duration: durationMs, startedAt: now, firedAt: null, elapsedBeforePause: 0, warnings: calculateCascadeWarnings(now + durationMs, [1], now), pomodoroState: { currentRound: nextRound, isBreak: false, isLongBreak: false, completedRounds: state.completedRounds, }, }; } else { // Work finished → start break const completedRounds = state.completedRounds + 1; const isLongBreak = completedRounds >= config.rounds; if (isLongBreak && completedRounds >= config.rounds) { // All rounds done, long break const durationMs = config.longBreakMinutes * 60 * 1000; return { ...timer, state: 'active', targetTime: now + durationMs, duration: durationMs, startedAt: now, firedAt: null, elapsedBeforePause: 0, warnings: [], pomodoroState: { currentRound: state.currentRound, isBreak: false, isLongBreak: true, completedRounds, }, }; } const durationMs = config.breakMinutes * 60 * 1000; return { ...timer, state: 'active', targetTime: now + durationMs, duration: durationMs, startedAt: now, firedAt: null, elapsedBeforePause: 0, warnings: [], pomodoroState: { currentRound: state.currentRound, isBreak: true, isLongBreak: false, completedRounds, }, }; } } // ── Utility ──────────────────────────────────────────────────── export function getRemainingMs(timer: Timer, now: number = Date.now()): number { if (timer.state === 'paused') { return (timer.duration ?? 0) - timer.elapsedBeforePause; } return Math.max(0, timer.targetTime - now); } export function isTimerActive(timer: Timer): boolean { return ['active', 'warning', 'snoozed'].includes(timer.state); } export function shouldTimerFire(timer: Timer, now: number = Date.now()): boolean { if (timer.state === 'snoozed' && timer.snoozedUntil && now >= timer.snoozedUntil) { return true; } if ((timer.state === 'active' || timer.state === 'warning') && now >= timer.targetTime) { return true; } return false; }