const DEFAULT_APP_URL = "http://127.0.0.1:3000"; const DEFAULT_WARNING_INTERVAL_MINUTES = 5; const DEFAULT_MORNING_BRIEF_TIME = "07:00"; const LOOP_INTERVAL_MS = 30_000; const appUrl = normalizeAppUrl(process.env.WTR_APP_URL ?? process.env.NEXT_PUBLIC_APP_URL ?? DEFAULT_APP_URL); const cronSecret = process.env.NOTIFICATIONS_CRON_SECRET ?? ""; const warningIntervalMinutes = readPositiveNumber(process.env.NOTIFICATIONS_WARNING_INTERVAL_MINUTES, DEFAULT_WARNING_INTERVAL_MINUTES); const morningBriefTime = normalizeTime(process.env.NOTIFICATIONS_MORNING_BRIEF_TIME ?? DEFAULT_MORNING_BRIEF_TIME); let lastWarningCheckAt = 0; let lastMorningBriefDate = ""; let isRunningTick = false; function normalizeAppUrl(value) { return value.replace(/\/+$/, ""); } function readPositiveNumber(value, fallback) { const number = Number(value); return Number.isFinite(number) && number > 0 ? number : fallback; } function normalizeTime(value) { return /^\d{2}:\d{2}$/.test(value) ? value : DEFAULT_MORNING_BRIEF_TIME; } function getWarsawDateParts(date = new Date()) { const parts = new Intl.DateTimeFormat("en-CA", { timeZone: "Europe/Warsaw", year: "numeric", month: "2-digit", day: "2-digit", hour: "2-digit", minute: "2-digit", hourCycle: "h23", }).formatToParts(date); const part = (type) => parts.find((entry) => entry.type === type)?.value ?? ""; return { dateKey: `${part("year")}-${part("month")}-${part("day")}`, time: `${part("hour")}:${part("minute")}`, }; } function isAtOrAfterTime(currentTime, targetTime) { return currentTime >= targetTime; } async function callNotificationEndpoint(path) { const response = await fetch(`${appUrl}${path}`, { headers: cronSecret ? { authorization: `Bearer ${cronSecret}` } : {}, }); const body = await response.text(); if (!response.ok) throw new Error(`${path} returned ${response.status}: ${body}`); return body; } async function tick() { if (isRunningTick) return; isRunningTick = true; try { const now = Date.now(); const warningIntervalMs = warningIntervalMinutes * 60_000; if (now - lastWarningCheckAt >= warningIntervalMs) { lastWarningCheckAt = now; await callNotificationEndpoint("/api/notifications/check"); } const warsawTime = getWarsawDateParts(); if (isAtOrAfterTime(warsawTime.time, morningBriefTime) && lastMorningBriefDate !== warsawTime.dateKey) { await callNotificationEndpoint("/api/notifications/daily-brief"); lastMorningBriefDate = warsawTime.dateKey; } } catch (error) { console.error(error); } finally { isRunningTick = false; } } console.log(`wtr. notification worker: ${appUrl}`); console.log(`Warnings every ${warningIntervalMinutes} min, morning brief at ${morningBriefTime} Europe/Warsaw.`); void tick(); setInterval(tick, LOOP_INTERVAL_MS);