Files
wtr/scripts/notification-worker.mjs
zv ee55521803
All checks were successful
CI / Lint, typecheck and build (push) Successful in 9m56s
chore: add prettier formatting
2026-06-14 20:26:56 +02:00

132 lines
4.3 KiB
JavaScript

import { existsSync, readFileSync } from "node:fs";
const DEFAULT_APP_URL = "http://127.0.0.1:3000";
const DEFAULT_WARNING_INTERVAL_MINUTES = 5;
const DEFAULT_BRIEF_INTERVAL_MINUTES = 5;
const DEFAULT_MORNING_BRIEF_TIME = "07:00";
const DEFAULT_TOMORROW_BRIEF_TIME = "18:00";
const LOOP_INTERVAL_MS = 30_000;
const initialEnvKeys = new Set(Object.keys(process.env));
loadEnvFile(".env", false);
loadEnvFile(".env.local", true);
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 briefIntervalMinutes = readPositiveNumber(
process.env.NOTIFICATIONS_BRIEF_INTERVAL_MINUTES,
DEFAULT_BRIEF_INTERVAL_MINUTES,
);
const morningBriefTime = normalizeTime(
process.env.NOTIFICATIONS_MORNING_BRIEF_TIME ?? DEFAULT_MORNING_BRIEF_TIME,
DEFAULT_MORNING_BRIEF_TIME,
);
const tomorrowBriefTime = normalizeTime(
process.env.NOTIFICATIONS_TOMORROW_BRIEF_TIME ?? DEFAULT_TOMORROW_BRIEF_TIME,
DEFAULT_TOMORROW_BRIEF_TIME,
);
let lastWarningCheckAt = 0;
let lastMorningBriefCheckAt = 0;
let lastTomorrowBriefCheckAt = 0;
let isRunningTick = false;
function loadEnvFile(path, overrideLoadedValues) {
if (!existsSync(path)) return;
const lines = readFileSync(path, "utf8").split(/\r?\n/);
for (const line of lines) {
const trimmedLine = line.trim();
if (!trimmedLine || trimmedLine.startsWith("#")) continue;
const separatorIndex = trimmedLine.indexOf("=");
if (separatorIndex === -1) continue;
const key = trimmedLine.slice(0, separatorIndex).trim();
const rawValue = trimmedLine.slice(separatorIndex + 1).trim();
if (!key || initialEnvKeys.has(key)) continue;
if (!overrideLoadedValues && process.env[key] !== undefined) continue;
process.env[key] = rawValue.replace(/^['"]|['"]$/g, "");
}
}
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, fallback) {
return /^\d{2}:\d{2}$/.test(value) ? value : fallback;
}
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 callNotificationEndpointSafely(path) {
try {
await callNotificationEndpoint(path);
return true;
} catch (error) {
console.error(formatWorkerError(error));
return false;
}
}
function formatWorkerError(error) {
if (error?.cause?.code === "ECONNREFUSED") {
return [
`wtr. notification worker: app is not reachable at ${appUrl}.`,
"Start the Next.js app first with `npm run start`, or set WTR_APP_URL to the running app URL.",
].join(" ");
}
if (error instanceof Error) return error.message;
return String(error);
}
async function tick() {
if (isRunningTick) return;
isRunningTick = true;
const now = Date.now();
const warningIntervalMs = warningIntervalMinutes * 60_000;
const briefIntervalMs = briefIntervalMinutes * 60_000;
try {
if (now - lastWarningCheckAt >= warningIntervalMs) {
lastWarningCheckAt = now;
await callNotificationEndpointSafely("/api/notifications/check");
}
if (now - lastMorningBriefCheckAt >= briefIntervalMs) {
lastMorningBriefCheckAt = now;
await callNotificationEndpointSafely("/api/notifications/daily-brief");
}
if (now - lastTomorrowBriefCheckAt >= briefIntervalMs) {
lastTomorrowBriefCheckAt = now;
await callNotificationEndpointSafely("/api/notifications/tomorrow-brief");
}
} catch (error) {
console.error(formatWorkerError(error));
} finally {
isRunningTick = false;
}
}
console.log(`wtr. notification worker: ${appUrl}`);
console.log(
`Warnings every ${warningIntervalMinutes} min. Brief checks every ${briefIntervalMinutes} min; targets: ${morningBriefTime} and ${tomorrowBriefTime} in each subscription timezone.`,
);
void tick();
setInterval(tick, LOOP_INTERVAL_MS);