110 lines
3.2 KiB
JavaScript
110 lines
3.2 KiB
JavaScript
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();
|
|
});
|