159 lines
5.2 KiB
TypeScript
159 lines
5.2 KiB
TypeScript
/**
|
|
* Cron utilities
|
|
* Parse schedules and estimate next run times
|
|
*/
|
|
import type { CronSchedule } from '@lib/cron-types';
|
|
|
|
export type ScheduleType = 'daily' | 'weekly' | 'monthly' | 'interval' | 'custom';
|
|
|
|
export interface SchedulePreset {
|
|
key: string;
|
|
value: string;
|
|
type: ScheduleType;
|
|
}
|
|
|
|
export const schedulePresets: SchedulePreset[] = [
|
|
{ key: 'everyMinute', value: '* * * * *', type: 'interval' },
|
|
{ key: 'every5Min', value: '*/5 * * * *', type: 'interval' },
|
|
{ key: 'every15Min', value: '*/15 * * * *', type: 'interval' },
|
|
{ key: 'everyHour', value: '0 * * * *', type: 'interval' },
|
|
{ key: 'daily9am', value: '0 9 * * *', type: 'daily' },
|
|
{ key: 'daily6pm', value: '0 18 * * *', type: 'daily' },
|
|
{ key: 'weeklyMon', value: '0 9 * * 1', type: 'weekly' },
|
|
{ key: 'monthly1st', value: '0 9 1 * *', type: 'monthly' },
|
|
];
|
|
|
|
// Parse cron schedule to human-readable format
|
|
// Handles both plain cron strings and Gateway CronSchedule objects
|
|
export function parseCronSchedule(
|
|
schedule: unknown,
|
|
t: (key: string, interpolations?: Record<string, unknown>) => string,
|
|
): string {
|
|
if (schedule && typeof schedule === 'object') {
|
|
const s = schedule as { kind?: string; expr?: string; tz?: string; everyMs?: number; at?: string };
|
|
if (s.kind === 'cron' && typeof s.expr === 'string') {
|
|
return parseCronExpr(s.expr, t);
|
|
}
|
|
if (s.kind === 'every' && typeof s.everyMs === 'number') {
|
|
const ms = s.everyMs;
|
|
if (ms < 60_000) return t('cron.schedule.everySeconds', { count: Math.round(ms / 1000) });
|
|
if (ms < 3_600_000) return t('cron.schedule.everyMinutes', { count: Math.round(ms / 60_000) });
|
|
if (ms < 86_400_000) return t('cron.schedule.everyHours', { count: Math.round(ms / 3_600_000) });
|
|
return t('cron.schedule.everyDays', { count: Math.round(ms / 86_400_000) });
|
|
}
|
|
if (s.kind === 'at' && typeof s.at === 'string') {
|
|
try {
|
|
return t('cron.schedule.onceAt', { time: new Date(s.at).toLocaleString() });
|
|
} catch {
|
|
return t('cron.schedule.onceAt', { time: s.at });
|
|
}
|
|
}
|
|
return String(schedule);
|
|
}
|
|
|
|
if (typeof schedule === 'string') {
|
|
return parseCronExpr(schedule, t);
|
|
}
|
|
|
|
return String(schedule ?? t('cron.schedule.unknown'));
|
|
}
|
|
|
|
export function parseCronExpr(
|
|
cron: string,
|
|
t: (key: string, interpolations?: Record<string, unknown>) => string,
|
|
): string {
|
|
const preset = schedulePresets.find((p) => p.value === cron);
|
|
if (preset) return t(`cron.presets.${preset.key}` as const);
|
|
|
|
const parts = cron.split(' ');
|
|
if (parts.length !== 5) return cron;
|
|
|
|
const [minute, hour, dayOfMonth, , dayOfWeek] = parts;
|
|
|
|
if (minute === '*' && hour === '*') return t('cron.presets.everyMinute');
|
|
if (minute.startsWith('*/')) return t('cron.schedule.everyMinutes', { count: Number(minute.slice(2)) });
|
|
if (hour === '*' && minute === '0') return t('cron.presets.everyHour');
|
|
if (dayOfWeek !== '*' && dayOfMonth === '*') {
|
|
return t('cron.schedule.weeklyAt', { day: dayOfWeek, time: `${hour}:${minute.padStart(2, '0')}` });
|
|
}
|
|
if (dayOfMonth !== '*') {
|
|
return t('cron.schedule.monthlyAtDay', { day: dayOfMonth, time: `${hour}:${minute.padStart(2, '0')}` });
|
|
}
|
|
if (hour !== '*') {
|
|
return t('cron.schedule.dailyAt', { time: `${hour}:${minute.padStart(2, '0')}` });
|
|
}
|
|
|
|
return cron;
|
|
}
|
|
|
|
export function estimateNextRun(scheduleExpr: string): string | null {
|
|
const now = new Date();
|
|
const next = new Date(now.getTime());
|
|
|
|
if (scheduleExpr === '* * * * *') {
|
|
next.setSeconds(0, 0);
|
|
next.setMinutes(next.getMinutes() + 1);
|
|
return formatLocaleDateTime(next);
|
|
}
|
|
|
|
if (scheduleExpr === '*/5 * * * *') {
|
|
const delta = 5 - (next.getMinutes() % 5 || 5);
|
|
next.setSeconds(0, 0);
|
|
next.setMinutes(next.getMinutes() + delta);
|
|
return formatLocaleDateTime(next);
|
|
}
|
|
|
|
if (scheduleExpr === '*/15 * * * *') {
|
|
const delta = 15 - (next.getMinutes() % 15 || 15);
|
|
next.setSeconds(0, 0);
|
|
next.setMinutes(next.getMinutes() + delta);
|
|
return formatLocaleDateTime(next);
|
|
}
|
|
|
|
if (scheduleExpr === '0 * * * *') {
|
|
next.setMinutes(0, 0, 0);
|
|
next.setHours(next.getHours() + 1);
|
|
return formatLocaleDateTime(next);
|
|
}
|
|
|
|
if (scheduleExpr === '0 9 * * *' || scheduleExpr === '0 18 * * *') {
|
|
const targetHour = scheduleExpr === '0 9 * * *' ? 9 : 18;
|
|
next.setSeconds(0, 0);
|
|
next.setHours(targetHour, 0, 0, 0);
|
|
if (next <= now) next.setDate(next.getDate() + 1);
|
|
return formatLocaleDateTime(next);
|
|
}
|
|
|
|
if (scheduleExpr === '0 9 * * 1') {
|
|
next.setSeconds(0, 0);
|
|
next.setHours(9, 0, 0, 0);
|
|
const day = next.getDay();
|
|
const daysUntilMonday = day === 1 ? 7 : (8 - day) % 7;
|
|
next.setDate(next.getDate() + daysUntilMonday);
|
|
return formatLocaleDateTime(next);
|
|
}
|
|
|
|
if (scheduleExpr === '0 9 1 * *') {
|
|
next.setSeconds(0, 0);
|
|
next.setDate(1);
|
|
next.setHours(9, 0, 0, 0);
|
|
if (next <= now) next.setMonth(next.getMonth() + 1);
|
|
return formatLocaleDateTime(next);
|
|
}
|
|
|
|
return null;
|
|
}
|
|
|
|
function formatLocaleDateTime(date: Date): string {
|
|
try {
|
|
return new Intl.DateTimeFormat(undefined, {
|
|
month: 'short',
|
|
day: 'numeric',
|
|
hour: '2-digit',
|
|
minute: '2-digit',
|
|
}).format(date);
|
|
} catch {
|
|
return date.toLocaleString();
|
|
}
|
|
}
|