import { prisma } from "@planarchy/db"; import { emitReminderDue, emitNotificationCreated } from "../sse/event-bus.js"; const POLL_INTERVAL_MS = 60_000; // 60 seconds function computeNextRemindAt(current: Date, recurrence: string): Date { const next = new Date(current); switch (recurrence) { case "daily": next.setDate(next.getDate() + 1); break; case "weekly": next.setDate(next.getDate() + 7); break; case "monthly": next.setMonth(next.getMonth() + 1); break; default: return current; } return next; } async function processReminders() { const now = new Date(); // Find all due reminders const dueReminders = await prisma.notification.findMany({ where: { category: "REMINDER", nextRemindAt: { lte: now }, }, take: 100, // process in batches }); for (const reminder of dueReminders) { try { if (reminder.recurrence) { // Recurring: create a new notification for this occurrence, advance nextRemindAt const notification = await prisma.notification.create({ data: { userId: reminder.userId, category: "NOTIFICATION", type: "REMINDER_DUE", priority: reminder.priority, title: reminder.title, body: reminder.body, entityId: reminder.entityId, entityType: reminder.entityType, link: reminder.link, sourceId: reminder.id, channel: "in_app", }, }); // Advance to next occurrence await prisma.notification.update({ where: { id: reminder.id }, data: { nextRemindAt: computeNextRemindAt(reminder.nextRemindAt!, reminder.recurrence), }, }); emitNotificationCreated(reminder.userId, notification.id); emitReminderDue(reminder.userId, notification.id); } else { // One-shot: mark the reminder as "fired" by clearing nextRemindAt await prisma.notification.update({ where: { id: reminder.id }, data: { nextRemindAt: null }, }); emitReminderDue(reminder.userId, reminder.id); emitNotificationCreated(reminder.userId, reminder.id); } } catch (err) { console.error(`[ReminderScheduler] Error processing reminder ${reminder.id}:`, err); } } } let intervalId: ReturnType | null = null; export function startReminderScheduler(): void { if (intervalId) return; // already running console.log("[ReminderScheduler] Starting (poll every 60s)"); // Run immediately to catch up on overdue reminders void processReminders(); intervalId = setInterval(() => void processReminders(), POLL_INTERVAL_MS); } export function stopReminderScheduler(): void { if (intervalId) { clearInterval(intervalId); intervalId = null; console.log("[ReminderScheduler] Stopped"); } }