JavaScripthardJavaScript

Cron-like Job Scheduler

Schedule tasks using cron-like syntax

01

The problem

Need to schedule tasks using cron syntax expressions

02

The solution

JavaScript
class CronScheduler {
  constructor() {
    this.jobs = new Map();
    this.timers = new Map();
    this.running = false;
  }
  
  // Parse cron expression
  parseCronExpression(expression) {
    const parts = expression.trim().split(/s+/);
    
    if (parts.length !== 5 && parts.length !== 6) {
      throw new Error('Invalid cron expression format');
    }
    
    const [minute, hour, dayOfMonth, month, dayOfWeek, ...rest] = parts;
    const year = parts.length === 6 ? rest[0] : '*';
    
    return {
      minute: this.parseField(minute, 0, 59),
      hour: this.parseField(hour, 0, 23),
      dayOfMonth: this.parseField(dayOfMonth, 1, 31),
      month: this.parseField(month, 1, 12),
      dayOfWeek: this.parseField(dayOfWeek, 0, 6), // 0 = Sunday
      year: year === '*' ? '*' : this.parseField(year, 1970, 2099)
    };
  }
  
  parseField(field, min, max) {
    if (field === '*') {
      return { type: 'any' };
    }
    
    // Handle ranges (1-5)
    if (field.includes('-')) {
      const [start, end] = field.split('-').map(Number);
      return { type: 'range', start, end };
    }
    
    // Handle steps (*/15)
    if (field.includes('/')) {
      const [range, step] = field.split('/');
      const stepNum = Number(step);
      
      if (range === '*') {
        return { type: 'step', step: stepNum };
      }
      
      // Handle ranges with steps (1-30/5)
      if (range.includes('-')) {
        const [start, end] = range.split('-').map(Number);
        return { type: 'rangeStep', start, end, step: stepNum };
      }
    }
    
    // Handle lists (1,3,5)
    if (field.includes(',')) {
      const values = field.split(',').map(Number);
      return { type: 'list', values };
    }
    
    // Single value
    const value = Number(field);
    return { type: 'value', value };
  }
  
  matchesCron(cron, date) {
    const fields = [
      { value: date.getMinutes(), cron: cron.minute },
      { value: date.getHours(), cron: cron.hour },
      { value: date.getDate(), cron: cron.dayOfMonth },
      { value: date.getMonth() + 1, cron: cron.month }, // JS months are 0-indexed
      { value: date.getDay(), cron: cron.dayOfWeek },
      { value: date.getFullYear(), cron: cron.year }
    ];
    
    return fields.every(({ value, cron }) => {
      if (cron.type === 'any') return true;
      if (cron.type === 'value') return value === cron.value;
      if (cron.type === 'range') return value >= cron.start && value <= cron.end;
      if (cron.type === 'list') return cron.values.includes(value);
      if (cron.type === 'step') return value % cron.step === 0;
      if (cron.type === 'rangeStep') {
        return value >= cron.start && 
               value <= cron.end && 
               (value - cron.start) % cron.step === 0;
      }
      return false;
    });
  }
  
  schedule(name, cronExpression, task) {
    const cron = this.parseCronExpression(cronExpression);
    
    this.jobs.set(name, { cron, task, expression: cronExpression });
    
    if (this.running) {
      this.scheduleNextExecution(name);
    }
    
    return this;
  }
  
  unschedule(name) {
    const timer = this.timers.get(name);
    if (timer) {
      clearTimeout(timer);
      this.timers.delete(name);
    }
    this.jobs.delete(name);
    return this;
  }
  
  scheduleNextExecution(name) {
    const job = this.jobs.get(name);
    if (!job) return;
    
    const now = new Date();
    let next = new Date(now.getTime() + 60000); // Start checking from next minute
    
    // Find next matching time (brute force approach)
    const maxAttempts = 10000; // Safety limit
    let attempts = 0;
    
    while (attempts < maxAttempts) {
      if (this.matchesCron(job.cron, next)) {
        const delay = next.getTime() - now.getTime();
        
        const timer = setTimeout(() => {
          try {
            job.task();
          } catch (error) {
            console.error(`Error executing job "${name}":`, error);
          }
          
          // Schedule next execution
          this.scheduleNextExecution(name);
        }, delay);
        
        this.timers.set(name, timer);
        return;
      }
      
      // Move to next minute
      next = new Date(next.getTime() + 60000);
      attempts++;
    }
    
    console.warn(`Could not find next execution time for job "${name}"`);
  }
  
  start() {
    if (this.running) return;
    
    this.running = true;
    for (const name of this.jobs.keys()) {
      this.scheduleNextExecution(name);
    }
    
    return this;
  }
  
  stop() {
    this.running = false;
    for (const timer of this.timers.values()) {
      clearTimeout(timer);
    }
    this.timers.clear();
    return this;
  }
  
  listJobs() {
    return Array.from(this.jobs.entries()).map(([name, job]) => ({
      name,
      expression: job.expression,
      nextExecution: this.getNextExecution(name)
    }));
  }
  
  getNextExecution(name) {
    const timer = this.timers.get(name);
    if (!timer) return null;
    
    // This is simplified - in reality we'd need to store the scheduled time
    return 'Scheduled';
  }
}

03

Put it to work

Example
const scheduler = new CronScheduler();

// Schedule jobs
scheduler.schedule('dailyBackup', '0 2 * * *', () => {
  console.log('Running daily backup at 2 AM');
});

scheduler.schedule('every5Minutes', '*/5 * * * *', () => {
  console.log('Running every 5 minutes');
});

scheduler.schedule('weekdaysAt9', '0 9 * * 1-5', () => {
  console.log('Running weekdays at 9 AM');
});

scheduler.schedule('firstOfMonth', '0 0 1 * *', () => {
  console.log('Running on the first day of each month');
});

// Start scheduler
scheduler.start();

// List all jobs
console.log(scheduler.listJobs());

// Stop after 10 minutes
setTimeout(() => {
  scheduler.stop();
  console.log('Scheduler stopped');
}, 600000);

// Unschedule a job
scheduler.unschedule('every5Minutes');