Files
starr-philproperties/cron/cronRegistry.util.js
T
kennethobsequio 1d12f04967 new commits
Signed-off-by: Kenneth Obsequio <k80308392@gmail.com>
2026-07-03 16:20:58 +08:00

83 lines
3.8 KiB
JavaScript

/***********************************************************************************************************************************************************************
* File Name : cronRegistry.util.js
* Type : Utility
* Description : Shared machinery for settings-backed cron jobs (the 4 jobs
* that emit notifications and are configurable from
* /admin/notifications/settings). Not every cron job in the
* app goes through this — jobs with no notification tied to
* them (e.g. lift_expired_bans) keep using node-cron directly.
*
* startSettingsBackedJobs() reads each job's schedule from
* cron_notification_settings (falling back to — and seeding —
* the job's own hardcoded default on first boot), then keeps
* a live reference to the scheduled task so it can be swapped
* out later via rescheduleJob() without a server restart.
*
* Author: Kenneth Obsequio (@lash0000)
* Date Created: Jul. 2, 2026
***********************************************************************************************************************************************************************/
'use strict';
const cron = require('node-cron');
const CronNotificationSetting = require('../models/notifications/cron_notification_setting.mdl');
// job_name -> { task: ScheduledTask, run: fn }
const runningTasks = new Map();
// CockroachDB can't run Sequelize's findOrCreate() — it wraps the insert in a
// pg_temp PL/pgSQL function to atomically catch unique_violation, which
// CockroachDB rejects ("cannot create user-defined functions under a temporary
// schema"). Plain findOne-then-create sidesteps it; the race window (two boots
// racing to seed the same job_name) is a non-issue here — jobs are seeded once.
async function getOrCreateSetting(jobName, defaultSchedule) {
let row = await CronNotificationSetting.findOne({ where: { job_name: jobName } });
if (row) return row;
try {
row = await CronNotificationSetting.create({ job_name: jobName, enabled: true, schedule: defaultSchedule });
} catch (err) {
row = await CronNotificationSetting.findOne({ where: { job_name: jobName } });
if (!row) throw err;
}
return row;
}
async function startSettingsBackedJobs(jobs, scopeLabel) {
const registered = [];
for (const { name, schedule: defaultSchedule, run } of jobs) {
let schedule = defaultSchedule;
try {
const settings = await getOrCreateSetting(name, defaultSchedule);
schedule = settings.schedule || defaultSchedule;
} catch (err) {
console.error(`[CRON][${scopeLabel}] Failed to load settings for "${name}", using hardcoded default:`, err);
}
if (!cron.validate(schedule)) {
console.error(`[CRON][${scopeLabel}] Invalid schedule for "${name}": "${schedule}" — skipped.`);
continue;
}
const task = cron.schedule(schedule, run);
runningTasks.set(name, { task, run });
registered.push({ name, scope: scopeLabel, schedule });
}
return registered;
}
// Live-swap a running job's schedule — used by notificationSettings.controller.js
// after an admin picks a new preset. No server restart required.
function rescheduleJob(jobName, newSchedule) {
const entry = runningTasks.get(jobName);
if (!entry) throw new Error(`No running cron task found for "${jobName}".`);
if (!cron.validate(newSchedule)) throw new Error(`Invalid cron schedule: "${newSchedule}".`);
entry.task.stop();
const task = cron.schedule(newSchedule, entry.run);
runningTasks.set(jobName, { task, run: entry.run });
}
module.exports = { startSettingsBackedJobs, rescheduleJob, getOrCreateSetting };