add backend hatchet worker for calendar predue sync

This commit is contained in:
Ruslan Bakiev
2026-03-08 19:15:30 +07:00
parent 0df426d5d6
commit e4870ce669
21 changed files with 1859 additions and 350 deletions

View File

@@ -38,6 +38,20 @@ export type TelegramOutboundRequest = {
businessConnectionId?: string | null;
};
export type CalendarPredueSyncResult = {
ok: boolean;
message: string;
now: string;
scanned: number;
updated: number;
skippedBeforeWindow: number;
skippedLocked: boolean;
preDueMinutes: number;
lookbackMinutes: number;
lookaheadMinutes: number;
lockKey: number;
};
function asString(value: unknown) {
if (typeof value !== "string") return null;
const v = value.trim();
@@ -54,6 +68,13 @@ function normalizeDirection(value: string): MessageDirection {
return value === "OUT" ? "OUT" : "IN";
}
function readIntEnv(name: string, defaultValue: number) {
const raw = asString(process.env[name]);
if (!raw) return defaultValue;
const parsed = Number.parseInt(raw, 10);
return Number.isFinite(parsed) ? parsed : defaultValue;
}
async function resolveTeamId(envelope: TelegramInboundEnvelope) {
const n = envelope.payloadNormalized;
const bcId = asString(n.businessConnectionId);
@@ -510,3 +531,107 @@ export async function requestTelegramOutbound(input: TelegramOutboundRequest) {
return { ok: true, message: "outbound_enqueued", runId: result.runId ?? null };
}
export async function syncCalendarPredueTimeline(): Promise<CalendarPredueSyncResult> {
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 lockRows = await prisma.$queryRaw<Array<{ locked: boolean }>>`
SELECT pg_try_advisory_lock(${lockKey}) AS locked
`;
const locked = Boolean(lockRows?.[0]?.locked);
if (!locked) {
return {
ok: true,
message: "lock_busy_skip",
now: now.toISOString(),
scanned: 0,
updated: 0,
skippedBeforeWindow: 0,
skippedLocked: true,
preDueMinutes,
lookbackMinutes,
lookaheadMinutes,
lockKey,
};
}
try {
const events = await prisma.calendarEvent.findMany({
where: {
isArchived: false,
contactId: { not: null },
startsAt: {
gte: rangeStart,
lte: rangeEnd,
},
},
orderBy: { startsAt: "asc" },
select: {
id: true,
teamId: true,
contactId: true,
startsAt: true,
},
});
let updated = 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,
},
});
updated += 1;
}
return {
ok: true,
message: "calendar_predue_synced",
now: now.toISOString(),
scanned: events.length,
updated,
skippedBeforeWindow,
skippedLocked: false,
preDueMinutes,
lookbackMinutes,
lookaheadMinutes,
lockKey,
};
} finally {
await prisma.$queryRaw`SELECT pg_advisory_unlock(${lockKey})`;
}
}