deploy/self-hosted/scheduler.mjs
Rama main · réplica de lectura del repositorio original.
const JOBS = [
["0 * * * *", "/api/cron/feedback-analysis"],
["* * * * *", "/api/cron/numo-turns"],
// Forge-relay delivery worker: retry backoff starts at 1 minute, so the
// worker must tick every minute to honor the at-least-once schedule.
["* * * * *", "/api/cron/forge-relay-deliveries"],
["35 * * * *", "/api/cron/forge-relay-maintenance"],
// Live page typing is journaled first, and live descriptions write their
// Markdown after typing rests; this writes what no request did.
["* * * * *", "/api/cron/page-materialization"],
["*/5 * * * *", "/api/cron/smart-assign"],
// Smart Fill re-reads an edited issue five minutes after its text rests.
["* * * * *", "/api/cron/smart-fill-reevaluation"],
["25 * * * *", "/api/cron/issue-index"],
["*/5 * * * *", "/api/cron/routines"],
["15 * * * *", "/api/cron/billing-sync"],
["15 * * * *", "/api/cron/encryption-maintenance"],
["30 15 * * *", "/api/cron/fx-rate"],
["45 3 * * *", "/api/cron/data-retention"],
];
const secret = process.env.CRON_SECRET;
const baseUrl = process.env.MINDDY_SCHEDULER_URL ?? "http://minddy:3000";
if (!secret || secret.length < 32) {
throw new Error("CRON_SECRET must contain at least 32 characters when scheduled jobs are enabled.");
}
function matchesField(expression, value) {
if (expression === "*") return true;
if (expression.startsWith("*/")) return value % Number(expression.slice(2)) === 0;
return expression.split(",").some((part) => Number(part) === value);
}
function matchesSchedule(schedule, now) {
const [minute, hour, dayOfMonth, month, dayOfWeek] = schedule.split(" ");
return [
matchesField(minute, now.getUTCMinutes()),
matchesField(hour, now.getUTCHours()),
matchesField(dayOfMonth, now.getUTCDate()),
matchesField(month, now.getUTCMonth() + 1),
matchesField(dayOfWeek, now.getUTCDay()),
].every(Boolean);
}
async function run(path) {
const response = await fetch(new URL(path, baseUrl), {
headers: { Authorization: `Bearer ${secret}` },
signal: AbortSignal.timeout(30_000),
});
console.log(`${new Date().toISOString()} ${path} ${response.status}`);
}
async function tick() {
const now = new Date();
await Promise.allSettled(JOBS.filter(([schedule]) => matchesSchedule(schedule, now)).map(([, path]) => run(path)));
}
const initialDelay = 60_000 - (Date.now() % 60_000);
setTimeout(() => {
void tick();
setInterval(() => void tick(), 60_000);
}, initialDelay);