import { PrismaClient } from "@prisma/client"; const prisma = new PrismaClient(); function readIntEnv(name, defaultValue) { const raw = String(process.env[name] ?? "").trim(); if (!raw) return defaultValue; const parsed = Number.parseInt(raw, 10); return Number.isFinite(parsed) ? parsed : defaultValue; } async function acquirePgLock(lockKey) { const rows = await prisma.$queryRawUnsafe("SELECT pg_try_advisory_lock($1) AS locked", lockKey); const first = Array.isArray(rows) ? rows[0] : null; return Boolean(first && (first.locked === true || first.locked === "t" || first.locked === 1)); } async function releasePgLock(lockKey) { await prisma.$queryRawUnsafe("SELECT pg_advisory_unlock($1)", lockKey).catch(() => undefined); } async function run() { const preDueMinutes = Math.max(1, readIntEnv("TIMELINE_EVENT_PREDUE_MINUTES", 30)); const lookbackMinutes = Math.max(preDueMinutes, readIntEnv("TIMELINE_EVENT_LOOKBACK_MINUTES", 180)); const lookaheadMinutes = Math.max(preDueMinutes, readIntEnv("TIMELINE_EVENT_LOOKAHEAD_MINUTES", 1440)); const lockKey = readIntEnv("TIMELINE_SCHEDULER_LOCK_KEY", 603001); const now = new Date(); const rangeStart = new Date(now.getTime() - lookbackMinutes * 60_000); const rangeEnd = new Date(now.getTime() + lookaheadMinutes * 60_000); const locked = await acquirePgLock(lockKey); if (!locked) { console.log(`[timeline-calendar-scheduler] skipped: lock ${lockKey} is busy`); return; } try { const events = await prisma.calendarEvent.findMany({ where: { isArchived: false, contactId: { not: null }, startsAt: { gte: rangeStart, lte: rangeEnd, }, }, select: { id: true, teamId: true, contactId: true, startsAt: true, }, orderBy: { startsAt: "asc" }, }); let touched = 0; let skippedBeforeWindow = 0; for (const event of events) { if (!event.contactId) continue; const preDueAt = new Date(event.startsAt.getTime() - preDueMinutes * 60_000); if (now < preDueAt) { skippedBeforeWindow += 1; continue; } await prisma.clientTimelineEntry.upsert({ where: { teamId_contentType_contentId: { teamId: event.teamId, contentType: "CALENDAR_EVENT", contentId: event.id, }, }, create: { teamId: event.teamId, contactId: event.contactId, contentType: "CALENDAR_EVENT", contentId: event.id, datetime: preDueAt, }, update: { contactId: event.contactId, datetime: preDueAt, }, }); touched += 1; } console.log( `[timeline-calendar-scheduler] done: scanned=${events.length} updated=${touched} skipped_before_window=${skippedBeforeWindow} at=${now.toISOString()}`, ); } finally { await releasePgLock(lockKey); } } run() .catch((error) => { const message = error instanceof Error ? error.stack || error.message : String(error); console.error(`[timeline-calendar-scheduler] failed: ${message}`); process.exitCode = 1; }) .finally(async () => { await prisma.$disconnect(); });