Spaces:
Sleeping
Sleeping
| import { db } from "@workspace/db"; | |
| import { tasksTable, notificationsTable } from "@workspace/db"; | |
| import { and, notInArray, gt, lte, eq, gte } from "drizzle-orm"; | |
| import { logger } from "./logger.js"; | |
| let isScanning = false; | |
| /** | |
| * Scans tasks to identify those due within 24 hours, sending idempotent notifications | |
| * to assigned users and creators. | |
| * | |
| * Returns the count of notifications successfully sent. | |
| */ | |
| export async function checkDeadlines(): Promise<number> { | |
| if (isScanning) { | |
| logger.info("A deadline check is already in progress, skipping concurrent run."); | |
| return 0; | |
| } | |
| isScanning = true; | |
| let sentCount = 0; | |
| try { | |
| const now = new Date(); | |
| const twentyFourHoursFromNow = new Date(now.getTime() + 24 * 60 * 60 * 1000); | |
| const startOfToday = new Date(); | |
| startOfToday.setHours(0, 0, 0, 0); | |
| logger.info({ now: now.toISOString(), twentyFourHoursFromNow: twentyFourHoursFromNow.toISOString() }, "Scanning database for upcoming deadlines..."); | |
| // Query active tasks due within 24 hours | |
| const matchingTasks = await db.select().from(tasksTable) | |
| .where(and( | |
| notInArray(tasksTable.status, ["completed", "cancelled"]), | |
| gt(tasksTable.dueDate, now), | |
| lte(tasksTable.dueDate, twentyFourHoursFromNow) | |
| )); | |
| logger.info({ matchCount: matchingTasks.length }, `Found ${matchingTasks.length} tasks due soon.`); | |
| for (const task of matchingTasks) { | |
| // Users to notify | |
| const userIdsToNotify: number[] = []; | |
| if (task.assignedUserId) userIdsToNotify.push(task.assignedUserId); | |
| if (task.createdByUserId && task.createdByUserId !== task.assignedUserId) { | |
| userIdsToNotify.push(task.createdByUserId); | |
| } | |
| for (const targetUserId of userIdsToNotify) { | |
| try { | |
| // Idempotency check: did we already notify this user for this task today? | |
| const existing = await db.select().from(notificationsTable) | |
| .where(and( | |
| eq(notificationsTable.userId, targetUserId), | |
| eq(notificationsTable.taskId, task.id), | |
| eq(notificationsTable.type, "deadline_soon"), | |
| gte(notificationsTable.createdAt, startOfToday) | |
| )); | |
| if (existing.length > 0) { | |
| logger.debug({ taskId: task.id, targetUserId }, "Idempotency trigger: deadline notification already sent today."); | |
| continue; | |
| } | |
| // Create notification | |
| const msg = `تنبيه: مهمة «${task.title}» موعد تسليمها غداً`; | |
| await db.insert(notificationsTable).values({ | |
| userId: targetUserId, | |
| taskId: task.id, | |
| type: "deadline_soon", | |
| message: msg, | |
| isRead: false | |
| }); | |
| logger.info({ taskId: task.id, targetUserId }, `Sent deadline_soon notification to user #${targetUserId} for task #${task.id}`); | |
| sentCount++; | |
| } catch (innerErr) { | |
| logger.error({ innerErr, taskId: task.id, targetUserId }, "Failed to send individual deadline notification"); | |
| } | |
| } | |
| } | |
| logger.info({ sentCount }, "Deadline scanning complete."); | |
| } catch (err) { | |
| logger.error({ err }, "Deadline checking operation encountered an error"); | |
| } finally { | |
| isScanning = false; | |
| } | |
| return sentCount; | |
| } | |
| /** | |
| * Sets up a self-scheduling setTimeout loop to run checkDeadlines() at 9:00 AM daily. | |
| */ | |
| export function scheduleDailyCheck() { | |
| const scheduleNext = () => { | |
| const now = new Date(); | |
| const nextRun = new Date(); | |
| nextRun.setHours(9, 0, 0, 0); | |
| // If 9:00 AM today has already passed, schedule for tomorrow | |
| if (now >= nextRun) { | |
| nextRun.setDate(nextRun.getDate() + 1); | |
| } | |
| const delay = nextRun.getTime() - now.getTime(); | |
| logger.info({ nextRun: nextRun.toISOString(), delayMs: delay }, "Deadline checker scheduled daily run"); | |
| setTimeout(async () => { | |
| try { | |
| logger.info("Running scheduled daily deadline check..."); | |
| const sent = await checkDeadlines(); | |
| logger.info({ sent }, "Scheduled daily deadline check execution completed."); | |
| } catch (err) { | |
| logger.error({ err }, "Scheduled daily deadline check execution failed"); | |
| } | |
| // Re-schedule for the next execution | |
| scheduleNext(); | |
| }, delay); | |
| }; | |
| scheduleNext(); | |
| } | |