Spaces:
Sleeping
Sleeping
File size: 4,353 Bytes
8314cf4 | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 | 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();
}
|