mirror of
https://github.com/kikootwo/ReadMeABook.git
synced 2026-06-03 21:00:09 +00:00
Initial commit
This commit is contained in:
@@ -0,0 +1,283 @@
|
||||
/**
|
||||
* Component: Cron Utilities
|
||||
* Documentation: documentation/backend/services/scheduler.md
|
||||
*/
|
||||
|
||||
export interface SchedulePreset {
|
||||
label: string;
|
||||
cron: string;
|
||||
description: string;
|
||||
}
|
||||
|
||||
export const SCHEDULE_PRESETS: SchedulePreset[] = [
|
||||
{ label: 'Every 15 minutes', cron: '*/15 * * * *', description: 'Runs 4 times per hour' },
|
||||
{ label: 'Every 30 minutes', cron: '*/30 * * * *', description: 'Runs twice per hour' },
|
||||
{ label: 'Every hour', cron: '0 * * * *', description: 'Runs at the start of every hour' },
|
||||
{ label: 'Every 2 hours', cron: '0 */2 * * *', description: 'Runs 12 times per day' },
|
||||
{ label: 'Every 3 hours', cron: '0 */3 * * *', description: 'Runs 8 times per day' },
|
||||
{ label: 'Every 6 hours', cron: '0 */6 * * *', description: 'Runs 4 times per day' },
|
||||
{ label: 'Every 12 hours', cron: '0 */12 * * *', description: 'Runs twice per day' },
|
||||
{ label: 'Daily at midnight', cron: '0 0 * * *', description: 'Runs once per day at 12:00 AM' },
|
||||
{ label: 'Daily at noon', cron: '0 12 * * *', description: 'Runs once per day at 12:00 PM' },
|
||||
{ label: 'Daily at 6 AM', cron: '0 6 * * *', description: 'Runs once per day at 6:00 AM' },
|
||||
{ label: 'Weekly (Sunday midnight)', cron: '0 0 * * 0', description: 'Runs once per week' },
|
||||
{ label: 'Monthly (1st at midnight)', cron: '0 0 1 * *', description: 'Runs once per month' },
|
||||
];
|
||||
|
||||
/**
|
||||
* Converts a cron expression to a human-readable description
|
||||
* @param cron - The cron expression (e.g., "0 *\/6 * * *")
|
||||
* @returns Human-readable description (e.g., "Every 6 hours")
|
||||
*/
|
||||
export function cronToHuman(cron: string): string {
|
||||
// Check if it matches a preset
|
||||
const preset = SCHEDULE_PRESETS.find(p => p.cron === cron);
|
||||
if (preset) {
|
||||
return preset.label;
|
||||
}
|
||||
|
||||
// Parse the cron expression
|
||||
const parts = cron.trim().split(/\s+/);
|
||||
if (parts.length < 5) {
|
||||
return cron; // Invalid cron, return as-is
|
||||
}
|
||||
|
||||
const [minute, hour, dayOfMonth, month, dayOfWeek] = parts;
|
||||
|
||||
// Minutes pattern
|
||||
if (minute.startsWith('*/')) {
|
||||
const interval = parseInt(minute.substring(2), 10);
|
||||
return `Every ${interval} minute${interval !== 1 ? 's' : ''}`;
|
||||
}
|
||||
|
||||
// Hours pattern
|
||||
if (hour.startsWith('*/') && minute === '0') {
|
||||
const interval = parseInt(hour.substring(2), 10);
|
||||
return `Every ${interval} hour${interval !== 1 ? 's' : ''}`;
|
||||
}
|
||||
|
||||
// Daily pattern
|
||||
if (dayOfMonth === '*' && month === '*' && dayOfWeek === '*') {
|
||||
if (hour === '*') {
|
||||
if (minute === '0') {
|
||||
return 'Every hour';
|
||||
}
|
||||
const minInterval = parseInt(minute.replace('*/', ''), 10);
|
||||
return `Every ${minInterval} minutes`;
|
||||
}
|
||||
|
||||
const hourNum = parseInt(hour, 10);
|
||||
if (!isNaN(hourNum)) {
|
||||
const minuteNum = parseInt(minute, 10);
|
||||
const time = formatTime(hourNum, minuteNum);
|
||||
return `Daily at ${time}`;
|
||||
}
|
||||
}
|
||||
|
||||
// Weekly pattern
|
||||
if (dayOfMonth === '*' && month === '*' && dayOfWeek !== '*') {
|
||||
const days = parseDayOfWeek(dayOfWeek);
|
||||
const hourNum = parseInt(hour, 10);
|
||||
const minuteNum = parseInt(minute, 10);
|
||||
const time = formatTime(hourNum, minuteNum);
|
||||
return `Weekly on ${days} at ${time}`;
|
||||
}
|
||||
|
||||
// Monthly pattern
|
||||
if (month === '*' && dayOfWeek === '*' && dayOfMonth !== '*') {
|
||||
const day = parseInt(dayOfMonth, 10);
|
||||
const hourNum = parseInt(hour, 10);
|
||||
const minuteNum = parseInt(minute, 10);
|
||||
const time = formatTime(hourNum, minuteNum);
|
||||
return `Monthly on day ${day} at ${time}`;
|
||||
}
|
||||
|
||||
// Fallback: return the cron expression
|
||||
return cron;
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse day of week number to name
|
||||
*/
|
||||
function parseDayOfWeek(dayOfWeek: string): string {
|
||||
const dayNames = ['Sunday', 'Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday'];
|
||||
const day = parseInt(dayOfWeek, 10);
|
||||
if (!isNaN(day) && day >= 0 && day <= 6) {
|
||||
return dayNames[day];
|
||||
}
|
||||
return dayOfWeek;
|
||||
}
|
||||
|
||||
/**
|
||||
* Format hour and minute to 12-hour time
|
||||
*/
|
||||
function formatTime(hour: number, minute: number): string {
|
||||
const period = hour >= 12 ? 'PM' : 'AM';
|
||||
const displayHour = hour === 0 ? 12 : hour > 12 ? hour - 12 : hour;
|
||||
const displayMinute = minute.toString().padStart(2, '0');
|
||||
return `${displayHour}:${displayMinute} ${period}`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Validates a cron expression
|
||||
* @param cron - The cron expression to validate
|
||||
* @returns true if valid, false otherwise
|
||||
*/
|
||||
export function isValidCron(cron: string): boolean {
|
||||
const parts = cron.trim().split(/\s+/);
|
||||
if (parts.length !== 5) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Basic validation - each part should be valid
|
||||
const [minute, hour, dayOfMonth, month, dayOfWeek] = parts;
|
||||
|
||||
return (
|
||||
isValidCronField(minute, 0, 59) &&
|
||||
isValidCronField(hour, 0, 23) &&
|
||||
isValidCronField(dayOfMonth, 1, 31) &&
|
||||
isValidCronField(month, 1, 12) &&
|
||||
isValidCronField(dayOfWeek, 0, 7)
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Validates a single cron field
|
||||
*/
|
||||
function isValidCronField(field: string, min: number, max: number): boolean {
|
||||
// Asterisk is always valid
|
||||
if (field === '*') {
|
||||
return true;
|
||||
}
|
||||
|
||||
// Step values (*/n)
|
||||
if (field.startsWith('*/')) {
|
||||
const step = parseInt(field.substring(2), 10);
|
||||
return !isNaN(step) && step > 0 && step <= max;
|
||||
}
|
||||
|
||||
// Range (n-m)
|
||||
if (field.includes('-')) {
|
||||
const [start, end] = field.split('-').map(s => parseInt(s, 10));
|
||||
return !isNaN(start) && !isNaN(end) && start >= min && end <= max && start < end;
|
||||
}
|
||||
|
||||
// List (n,m,o)
|
||||
if (field.includes(',')) {
|
||||
const values = field.split(',').map(s => parseInt(s, 10));
|
||||
return values.every(v => !isNaN(v) && v >= min && v <= max);
|
||||
}
|
||||
|
||||
// Single value
|
||||
const value = parseInt(field, 10);
|
||||
return !isNaN(value) && value >= min && value <= max;
|
||||
}
|
||||
|
||||
/**
|
||||
* Custom schedule builder
|
||||
*/
|
||||
export interface CustomSchedule {
|
||||
type: 'minutes' | 'hours' | 'daily' | 'weekly' | 'monthly' | 'custom';
|
||||
interval?: number; // For minutes/hours
|
||||
time?: { hour: number; minute: number }; // For daily/weekly/monthly
|
||||
dayOfWeek?: number; // For weekly (0-6)
|
||||
dayOfMonth?: number; // For monthly (1-31)
|
||||
customCron?: string; // For custom
|
||||
}
|
||||
|
||||
/**
|
||||
* Converts custom schedule to cron expression
|
||||
*/
|
||||
export function customScheduleToCron(schedule: CustomSchedule): string {
|
||||
switch (schedule.type) {
|
||||
case 'minutes':
|
||||
return `*/${schedule.interval || 15} * * * *`;
|
||||
|
||||
case 'hours':
|
||||
const hourInterval = schedule.interval || 1;
|
||||
// If interval is 24 or more hours, convert to daily at midnight
|
||||
if (hourInterval >= 24) {
|
||||
return `0 0 * * *`; // Daily at midnight
|
||||
}
|
||||
return `0 */${hourInterval} * * *`;
|
||||
|
||||
case 'daily':
|
||||
const dailyHour = schedule.time?.hour || 0;
|
||||
const dailyMinute = schedule.time?.minute || 0;
|
||||
return `${dailyMinute} ${dailyHour} * * *`;
|
||||
|
||||
case 'weekly':
|
||||
const weeklyHour = schedule.time?.hour || 0;
|
||||
const weeklyMinute = schedule.time?.minute || 0;
|
||||
const weeklyDay = schedule.dayOfWeek || 0;
|
||||
return `${weeklyMinute} ${weeklyHour} * * ${weeklyDay}`;
|
||||
|
||||
case 'monthly':
|
||||
const monthlyHour = schedule.time?.hour || 0;
|
||||
const monthlyMinute = schedule.time?.minute || 0;
|
||||
const monthlyDay = schedule.dayOfMonth || 1;
|
||||
return `${monthlyMinute} ${monthlyHour} ${monthlyDay} * *`;
|
||||
|
||||
case 'custom':
|
||||
return schedule.customCron || '0 * * * *';
|
||||
|
||||
default:
|
||||
return '0 * * * *';
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Attempts to parse a cron expression into a custom schedule
|
||||
*/
|
||||
export function cronToCustomSchedule(cron: string): CustomSchedule {
|
||||
const parts = cron.trim().split(/\s+/);
|
||||
if (parts.length !== 5) {
|
||||
return { type: 'custom', customCron: cron };
|
||||
}
|
||||
|
||||
const [minute, hour, dayOfMonth, month, dayOfWeek] = parts;
|
||||
|
||||
// Minutes pattern
|
||||
if (minute.startsWith('*/') && hour === '*' && dayOfMonth === '*' && month === '*' && dayOfWeek === '*') {
|
||||
const interval = parseInt(minute.substring(2), 10);
|
||||
return { type: 'minutes', interval };
|
||||
}
|
||||
|
||||
// Hours pattern
|
||||
if (hour.startsWith('*/') && minute === '0' && dayOfMonth === '*' && month === '*' && dayOfWeek === '*') {
|
||||
const interval = parseInt(hour.substring(2), 10);
|
||||
return { type: 'hours', interval };
|
||||
}
|
||||
|
||||
// Daily pattern
|
||||
if (dayOfMonth === '*' && month === '*' && dayOfWeek === '*' && !hour.includes('*') && !minute.includes('*')) {
|
||||
const hourNum = parseInt(hour, 10);
|
||||
const minuteNum = parseInt(minute, 10);
|
||||
if (!isNaN(hourNum) && !isNaN(minuteNum)) {
|
||||
return { type: 'daily', time: { hour: hourNum, minute: minuteNum } };
|
||||
}
|
||||
}
|
||||
|
||||
// Weekly pattern
|
||||
if (dayOfMonth === '*' && month === '*' && !dayOfWeek.includes('*') && !hour.includes('*') && !minute.includes('*')) {
|
||||
const hourNum = parseInt(hour, 10);
|
||||
const minuteNum = parseInt(minute, 10);
|
||||
const dayNum = parseInt(dayOfWeek, 10);
|
||||
if (!isNaN(hourNum) && !isNaN(minuteNum) && !isNaN(dayNum)) {
|
||||
return { type: 'weekly', time: { hour: hourNum, minute: minuteNum }, dayOfWeek: dayNum };
|
||||
}
|
||||
}
|
||||
|
||||
// Monthly pattern
|
||||
if (month === '*' && dayOfWeek === '*' && !dayOfMonth.includes('*') && !hour.includes('*') && !minute.includes('*')) {
|
||||
const hourNum = parseInt(hour, 10);
|
||||
const minuteNum = parseInt(minute, 10);
|
||||
const dayNum = parseInt(dayOfMonth, 10);
|
||||
if (!isNaN(hourNum) && !isNaN(minuteNum) && !isNaN(dayNum)) {
|
||||
return { type: 'monthly', time: { hour: hourNum, minute: minuteNum }, dayOfMonth: dayNum };
|
||||
}
|
||||
}
|
||||
|
||||
// Fallback to custom
|
||||
return { type: 'custom', customCron: cron };
|
||||
}
|
||||
Reference in New Issue
Block a user