add backend hatchet worker for calendar predue sync
This commit is contained in:
@@ -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})`;
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user