JSPM

@jamx-framework/scheduler

1.0.0
  • ESM via JSPM
  • ES Module Entrypoint
  • Export Map
  • Keywords
  • License
  • Repository URL
  • TypeScript Types
  • README
  • Created
  • Published
  • Downloads 8
  • Score
    100M100P100Q68753F
  • License MIT

JAMX Framework — Cron job scheduler

Package Exports

  • @jamx-framework/scheduler

Readme

@jamx-framework/scheduler

Descripción

Sistema de programación de tareas (scheduler) para JAMX Framework. Permite ejecutar tareas programadas usando expresiones cron o intervalos de tiempo fijos. Ideal para jobs de mantenimiento, limpieza, reportes, sincronización de datos, y cualquier proceso que necesite ejecutarse automáticamente en segundo plano.

Cómo funciona

El scheduler soporta dos tipos de tareas:

  1. Cron tasks: Se ejecutan según expresiones cron (ej: "0 0 * * *" para diario a medianoche)
  2. Interval tasks: Se ejecutan cada N milisegundos (ej: cada 5 minutos)

El scheduler mantiene un ticker interno que verifica cada segundo si alguna tarea cron debe ejecutarse, y usa setInterval para tareas con intervalo fijo.

Componentes principales

Scheduler (src/scheduler.ts)

Clase principal para gestionar tareas programadas:

  • add(definition): Registra una nueva tarea
  • remove(name): Elimina una tarea
  • start(): Inicia la ejecución de tareas
  • stop(): Detiene todas las tareas
  • run(name): Ejecuta una tarea manualmente
  • statusAll(): Retorna estado de todas las tareas
  • taskStatus(name): Retorna estado de una tarea específica

Cron Parser (src/cron-parser.ts)

Parseo y evaluación de expresiones cron:

  • parseCron(expression): Convierte string cron a CronExpression
  • matchesCron(expr, date): Verifica si una fecha coincide con la expresión
  • nextMatch(expr, from): Calcula próxima ejecución

Uso básico

Crear un scheduler

import { Scheduler } from '@jamx-framework/scheduler';

const scheduler = new Scheduler();

Añadir tareas cron

// Tarea que se ejecuta cada día a las 3:00 AM
scheduler.add({
  name: 'daily-cleanup',
  cron: '0 3 * * *',
  handler: async () => {
    console.log('Running daily cleanup...');
    await cleanupOldRecords();
  },
  onError: (err) => {
    console.error('Cleanup failed:', err);
  },
  runOnInit: false, // no ejecutar al iniciar
});

// Tarea cada lunes a las 8:00 AM
scheduler.add({
  name: 'weekly-report',
  cron: '0 8 * * 1', // 1 = lunes
  handler: async () => {
    await generateWeeklyReport();
  },
});

// Usar alias
scheduler.add({
  name: 'midnight-task',
  cron: '@daily', // equivalente a '0 0 * * *'
  handler: () => {
    console.log('Running at midnight');
  },
});

Añadir tareas de intervalo

// Cada 5 minutos
scheduler.add({
  name: 'cache-refresh',
  every: 5 * 60 * 1000, // 5 minutos en ms
  handler: async () => {
    await refreshCache();
  },
  onError: (err) => {
    console.error('Cache refresh failed:', err);
  },
});

// Cada 30 segundos
scheduler.add({
  name: 'health-check',
  every: 30_000,
  handler: async () => {
    const healthy = await checkHealth();
    if (!healthy) {
      await sendAlert();
    }
  },
  runOnInit: true, // ejecutar inmediatamente al iniciar
});

Iniciar y detener

// Iniciar scheduler
scheduler.start();
console.log('Scheduler started');

// Detener scheduler
await scheduler.stop();
console.log('Scheduler stopped');

Ejecutar tareas manualmente

// Ejecutar una tarea específica
await scheduler.run('daily-cleanup');

// Ver estado de todas las tareas
const allStatus = scheduler.statusAll();
console.log(allStatus);
/*
[
  {
    name: 'daily-cleanup',
    lastRun: 1700000000000,
    nextRun: 1700086400000,
    runs: 5,
    errors: 0,
  },
  ...
]
*/

// Ver estado de una tarea
const status = scheduler.taskStatus('cache-refresh');
console.log(status);
/*
{
  name: 'cache-refresh',
  lastRun: 1700000000000,
  nextRun: 170000300000,
  runs: 100,
  errors: 2,
}
*/

Eliminar tareas

const removed = scheduler.remove('old-task');
if (removed) {
  console.log('Task removed');
}

API Reference

Tipos

TaskHandler

type TaskHandler = () => void | Promise<void>;

Función que se ejecuta cuando la tarea dispara.

CronTask

interface CronTask {
  name: string;
  cron: string;
  handler: TaskHandler;
  onError?: (err: unknown) => void;
  runOnInit?: boolean;
}

Tarea basada en expresión cron.

IntervalTask

interface IntervalTask {
  name: string;
  every: number; // milisegundos
  handler: TaskHandler;
  onError?: (err: unknown) => void;
  runOnInit?: boolean;
}

Tarea basada en intervalo.

TaskDefinition

type TaskDefinition = CronTask | IntervalTask;

Definición de tarea (cron o intervalo).

TaskStatus

interface TaskStatus {
  name: string;
  lastRun: number | null; // timestamp de última ejecución
  nextRun: number | null; // timestamp de próxima ejecución
  runs: number; // número de ejecuciones
  errors: number; // número de errores
}

Estado de una tarea.

SchedulerStatus

type SchedulerStatus = "idle" | "running" | "stopped";

Estado del scheduler.

Clase Scheduler

Constructor

new Scheduler()

Crea un scheduler vacío.

add()

add(definition: TaskDefinition): this

Registra una nueva tarea. Lanza error si ya existe una tarea con el mismo nombre.

Ejemplo:

scheduler.add({
  name: 'my-task',
  cron: '0 * * * *', // cada hora
  handler: async () => {
    await doSomething();
  },
});

remove()

remove(name: string): boolean

Elimina una tarea. Retorna true si la tarea existía y fue eliminada, false si no existía.

start()

start(): void

Inicia el scheduler:

  • Ejecuta todas las tareas con runOnInit: true
  • Inicia ticker cada segundo para tareas cron
  • Inicia intervalos para tareas de intervalo

Nota: Si el scheduler ya está running, no hace nada.

stop()

stop(): void

Detiene el scheduler:

  • Cancela ticker de cron
  • Cancela todos los intervalos
  • Cambia estado a "stopped"

run()

async run(name: string): Promise<void>

Ejecuta una tarea manualmente por nombre. Lanza error si la tarea no existe.

statusAll()

statusAll(): TaskStatus[]

Retorna array con el estado de todas las tareas registradas.

taskStatus()

taskStatus(name: string): TaskStatus | null

Retorna el estado de una tarea específica, o null si no existe.

isRunning

readonly isRunning: boolean

true si el scheduler está en ejecución.

Funciones de Cron Parser

parseCron()

function parseCron(expression: string): CronExpression

Parsea una expresión cron y retorna un objeto CronExpression con arrays de valores para cada campo.

Formato cron: minute hour day-of-month month day-of-week

Ejemplos:

parseCron('0 0 * * *'); // cada día a medianoche
parseCron('*/5 * * * *'); // cada 5 minutos
parseCron('0 9-17 * * 1-5'); // cada hora de 9 a 17, de lunes a viernes
parseCron('@daily'); // alias para '0 0 * * *'

CronExpression:

interface CronExpression {
  minutes: number[];    // 0-59
  hours: number[];      // 0-23
  daysOfMonth: number[]; // 1-31
  months: number[];     // 1-12
  daysOfWeek: number[]; // 0-7 (0 y 7 = domingo)
}

matchesCron()

function matchesCron(expr: CronExpression, date: Date): boolean

Verifica si una fecha específica coincide con la expresión cron.

Ejemplo:

const expr = parseCron('0 0 * * *'); // medianoche
const now = new Date('2024-01-15T00:00:00');
matchesCron(expr, now); // true

nextMatch()

function nextMatch(expr: CronExpression, from: Date): Date

Calcula la próxima fecha/hora que coincide con la expresión cron, a partir de from.

Ejemplo:

const expr = parseCron('0 0 * * *'); // medianoche
const now = new Date('2024-01-15T14:30:00');
const next = nextMatch(expr, now); // 2024-01-16T00:00:00

Expresiones Cron

Formato estándar

* * * * *
│ │ │ │ │
│ │ │ │ └─ Día de la semana (0-7, 0 y 7 = domingo)
│ │ │ └─── Mes (1-12)
│ │ └───── Día del mes (1-31)
│ └─────── Hora (0-23)
└───────── Minuto (0-59)

Comodines y rangos

  • *: Cualquier valor
  • */n: Cada n unidades (ej: */5 en minutos = cada 5 minutos)
  • a-b: Rango (ej: 9-17 = de 9 a 17)
  • a,b,c: Lista (ej: 1,15,30 = días 1, 15 y 30)
  • */n + a-b: Combinación (ej: */2 9-17 * * 1-5)

Ejemplos comunes

Expresión Descripción
0 0 * * * Diario a medianoche
0 3 * * * Diario a las 3:00 AM
0 0 * * 0 Cada domingo a medianoche
0 9 * * 1 Cada lunes a las 9:00 AM
*/15 * * * * Cada 15 minutos
0 */2 * * * Cada 2 horas
0 0 1 * * El primer día de cada mes
0 0 1 1 * El 1 de enero (Año Nuevo)
@hourly Cada hora (alias)
@daily Diario a medianoche (alias)
@weekly Semanalmente el domingo a medianoche (alias)
@monthly Mensualmente el día 1 a medianoche (alias)
@yearly Anualmente el 1 de enero a medianoche (alias)

Días de la semana

  • 0 o 7 = Domingo
  • 1 = Lunes
  • 2 = Martes
  • 3 = Miércoles
  • 4 = Jueves
  • 5 = Viernes
  • 6 = Sábado

Meses

  • 1 = Enero
  • 2 = Febrero
  • ...
  • 12 = Diciembre

Flujo interno

Inicialización

const scheduler = new Scheduler();
scheduler.add({ name: 'task1', cron: '0 0 * * *', handler: () => {} });
scheduler.start();

Dentro de start():

start() {
  this.status = "running";

  // 1. Ejecutar tareas con runOnInit
  for (const task of this.tasks.values()) {
    if (task.runOnInit) {
      void this.runTask(task);
    }
  }

  // 2. Iniciar ticker para cron (cada segundo)
  this.tickInterval = setInterval(() => this.tick(), 1000);

  // 3. Iniciar intervalos para tareas de intervalo
  for (const task of this.tasks.values()) {
    if (task.every !== undefined) {
      task.timer = setInterval(() => void this.runTask(task), task.every);
    }
  }
}

Ticker de cron

private tick(): void {
  const now = Date.now();

  for (const task of this.tasks.values()) {
    // Solo tareas cron
    if (!task.cronExpr) continue;
    if (!task.nextRun) continue;
    if (now < task.nextRun) continue;

    // Ejecutar tarea
    void this.runTask(task);

    // Calcular próxima ejecución
    task.nextRun = nextMatch(task.cronExpr, new Date()).getTime();
  }
}

Ejecución de tarea

private async runTask(task: InternalTask): Promise<void> {
  task.lastRun = Date.now();
  task.runs++;

  try {
    await task.handler();
  } catch (err) {
    task.errors++;
    if (task.onError) {
      task.onError(err);
    }
  }
}

Ejemplos completos

Sistema de backup diario

import { Scheduler } from '@jamx-framework/scheduler';
import { backupDatabase } from './services/backup.js';
import { uploadToS3 } from './services/s3.js';

const scheduler = new Scheduler();

scheduler.add({
  name: 'daily-backup',
  cron: '0 2 * * *', // 2:00 AM cada día
  handler: async () => {
    console.log('Starting backup...');
    const backup = await backupDatabase();
    await uploadToS3(backup, `backup-${Date.now()}.sql`);
    console.log('Backup completed');
  },
  onError: (err) => {
    console.error('Backup failed:', err);
    // Enviar alerta
    sendAlert('Backup failed', String(err));
  },
  runOnInit: false,
});

scheduler.start();

Health check cada minuto

import { Scheduler } from '@jamx-framework/scheduler';
import { checkDatabase, checkRedis, checkExternalAPI } from './health.js';

scheduler.add({
  name: 'health-check',
  every: 60_000, // cada minuto
  handler: async () => {
    const checks = await Promise.all([
      checkDatabase(),
      checkRedis(),
      checkExternalAPI(),
    ]);

    const allHealthy = checks.every(c => c.healthy);
    if (!allHealthy) {
      await sendSlackAlert('Health check failed!');
    }
  },
  onError: (err) => {
    console.error('Health check error:', err);
  },
  runOnInit: true, // ejecutar inmediatamente
});

scheduler.start();

Procesador de cola con intervalo

import { Scheduler } from '@jamx-framework/scheduler';
import { queue } from './queue.js';

scheduler.add({
  name: 'queue-worker',
  every: 5_000, // cada 5 segundos
  handler: async () => {
    // Procesar hasta 10 jobs a la vez
    for (let i = 0; i < 10; i++) {
      const job = await queue.dequeue();
      if (!job) break;

      try {
        await processJob(job);
      } catch (err) {
        await queue.fail(job.id, err);
      }
    }
  },
  runOnInit: true,
});

scheduler.start();

Reporte semanal

import { Scheduler } from '@jamx-framework/scheduler';
import { generateWeeklyReport } from './reports.js';
import { sendEmail } from './mailer.js';

scheduler.add({
  name: 'weekly-report',
  cron: '0 8 * * 1', // Lunes 8:00 AM
  handler: async () => {
    const report = await generateWeeklyReport();
    await sendEmail({
      to: 'team@company.com',
      subject: 'Weekly Report',
      html: report,
    });
  },
  onError: (err) => {
    console.error('Weekly report failed:', err);
  },
});

scheduler.start();

Limpieza de sesiones expiradas

import { Scheduler } from '@jamx-framework/scheduler';
import { db } from './db.js';

scheduler.add({
  name: 'cleanup-sessions',
  cron: '0 */6 * * *', // cada 6 horas
  handler: async () => {
    const deleted = await db.sessions.deleteWhere({
      expiresAt: { $lt: Date.now() },
    });
    console.log(`Deleted ${deleted} expired sessions`);
  },
});

scheduler.start();

Múltiples tareas con configuración centralizada

import { Scheduler } from '@jamx-framework/scheduler';

const tasks = [
  {
    name: 'cache-warmup',
    cron: '*/10 * * * *', // cada 10 minutos
    handler: () => import('./services/cache-warmup.js').then(m => m.warmup()),
  },
  {
    name: 'metrics-aggregation',
    cron: '0 * * * *', // cada hora
    handler: () => import('./services/metrics.js').then(m => m.aggregate()),
  },
  {
    name: 'log-rotation',
    cron: '0 0 * * *', // medianoche
    handler: () => import('./services/logs.js').then(m => m.rotate()),
  },
];

const scheduler = new Scheduler();

for (const task of tasks) {
  scheduler.add({
    ...task,
    onError: (err) => {
      console.error(`Task ${task.name} failed:`, err);
    },
  });
}

scheduler.start();

Consideraciones de rendimiento

Ticker de cron

  • El ticker corre cada segundo (1000ms)
  • Para la mayoría de casos esto es suficiente precisión
  • Si necesitas precisión de milisegundos, usa interval tasks

Interval tasks

  • Usan setInterval nativo de Node.js
  • No acumulan retraso (Node.js los ejecuta puntualmente)
  • Si el handler tarda más que el intervalo, se superponen ejecuciones

Manejo de errores

  • Los errores en handlers no detienen el scheduler
  • Se capturan y se incrementa el contador errors
  • onError permite logging o alertas personalizadas

Memoria

  • El scheduler mantiene un Map de tareas en memoria
  • No hay persistencia; al reiniciar el proceso se pierden los timers
  • Considera usar runOnInit: true para tareas críticas

Timezone

  • Las expresiones cron usan el timezone del sistema (Node.js)
  • Para timezone específico, ajusta la fecha en nextMatch() o usa process.TZ

Testing

Tests unitarios

import { Scheduler, parseCron, matchesCron, nextMatch } from '@jamx-framework/scheduler';
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';

describe('Scheduler', () => {
  let scheduler: Scheduler;

  beforeEach(() => {
    scheduler = new Scheduler();
  });

  it('should add and remove tasks', () => {
    scheduler.add({
      name: 'test-task',
      cron: '0 * * * *',
      handler: () => {},
    });

    expect(scheduler.taskStatus('test-task')).not.toBeNull();

    scheduler.remove('test-task');
    expect(scheduler.taskStatus('test-task')).toBeNull();
  });

  it('should start and stop', () => {
    expect(scheduler.isRunning).toBe(false);

    scheduler.start();
    expect(scheduler.isRunning).toBe(true);

    scheduler.stop();
    expect(scheduler.isRunning).toBe(false);
  });

  it('should track task status', async () => {
    let callCount = 0;
    scheduler.add({
      name: 'counter',
      every: 1000,
      handler: () => {
        callCount++;
      },
      runOnInit: true,
    });

    scheduler.start();

    await new Promise(r => setTimeout(r, 2500));
    scheduler.stop();

    expect(callCount).toBeGreaterThanOrEqual(2); // init + al menos 1 intervalo
  });
});

describe('Cron Parser', () => {
  it('should parse simple cron', () => {
    const expr = parseCron('0 0 * * *');
    expect(expr.minutes).toEqual([0]);
    expect(expr.hours).toEqual([0]);
    expect(expr.daysOfMonth).toEqual([1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31]);
    expect(expr.months).toEqual([1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12]);
    expect(expr.daysOfWeek).toEqual([0, 1, 2, 3, 4, 5, 6]);
  });

  it('should parse ranges', () => {
    const expr = parseCron('0 9-17 * * 1-5');
    expect(expr.hours).toEqual([9, 10, 11, 12, 13, 14, 15, 16, 17]);
    expect(expr.daysOfWeek).toEqual([1, 2, 3, 4, 5]);
  });

  it('should parse step values', () => {
    const expr = parseCron('*/5 * * * *');
    expect(expr.minutes).toEqual([0, 5, 10, 15, 20, 25, 30, 35, 40, 45, 50, 55]);
  });

  it('should support aliases', () => {
    const expr = parseCron('@daily');
    expect(expr.minutes).toEqual([0]);
    expect(expr.hours).toEqual([0]);
  });

  it('should calculate next match', () => {
    const expr = parseCron('0 0 * * *'); // medianoche
    const from = new Date('2024-01-15T14:30:00');
    const next = nextMatch(expr, from);
    expect(next.getHours()).toBe(0);
    expect(next.getMinutes()).toBe(0);
    expect(next.getDate()).toBe(16); // siguiente día
  });
});

Mock de tiempo en tests

import { vi } from 'vitest';

// Mock de timers
vi.useFakeTimers();

const scheduler = new Scheduler();
scheduler.add({
  name: 'fast-task',
  every: 1000,
  handler: vi.fn(),
});

scheduler.start();

// Avanzar tiempo 3 segundos
vi.advanceTimersByTime(3000);

expect(scheduler.taskStatus('fast-task')?.runs).toBe(3);

scheduler.stop();
vi.useRealTimers();

Buenas prácticas

1. Nombres descriptivos

// ❌ Mal
scheduler.add({ name: 'task1', cron: '0 0 * * *', handler: doStuff });

// ✅ Bien
scheduler.add({
  name: 'daily-user-cleanup',
  cron: '0 3 * * *',
  handler: cleanupInactiveUsers,
});

2. Manejo de errores

scheduler.add({
  name: 'critical-task',
  cron: '0 * * * *',
  handler: async () => {
    try {
      await doCriticalWork();
    } catch (err) {
      // Log y alerta
      logger.error('Critical task failed', err);
      await sendAlert('Critical task failed', err);
      throw err; // para que onError también lo capture
    }
  },
  onError: (err) => {
    logger.error('Task error:', err);
  },
});

3. Tiempos de ejecución

// Asegurar que el handler no se superponga
scheduler.add({
  name: 'long-task',
  every: 5 * 60 * 1000, // cada 5 minutos
  handler: async () => {
    // Si la tarea puede tardar más de 5 min, usar lock
    const lock = await acquireLock('long-task');
    if (!lock) {
      console.log('Skipping: previous execution still running');
      return;
    }

    try {
      await doLongRunningWork();
    } finally {
      await lock.release();
    }
  },
});

4. Graceful shutdown

import { Scheduler } from '@jamx-framework/scheduler';

const scheduler = new Scheduler();
// ... añadir tareas

// Manejar señales de terminación
process.on('SIGTERM', async () => {
  console.log('Received SIGTERM, stopping scheduler...');
  await scheduler.stop();
  process.exit(0);
});

process.on('SIGINT', async () => {
  console.log('Received SIGINT, stopping scheduler...');
  await scheduler.stop();
  process.exit(0);
});

scheduler.start();

5. Monitoreo

// Exponer métricas
import { Metrics } from '@jamx-framework/metrics';

scheduler.add({
  name: 'data-sync',
  cron: '0 */6 * * *',
  handler: async () => {
    const start = Date.now();
    try {
      await syncData();
      Metrics.increment('scheduler.task.success', { task: 'data-sync' });
    } catch (err) {
      Metrics.increment('scheduler.task.error', { task: 'data-sync' });
      throw err;
    } finally {
      const duration = Date.now() - start;
      Metrics.histogram('scheduler.task.duration', duration, { task: 'data-sync' });
    }
  },
});

Limitaciones

Precisión de tiempo

  • Cron tasks: precisión de 1 segundo (ticker cada segundo)
  • Interval tasks: precisión de ~1ms (depende de event loop de Node.js)
  • No garantía de ejecución exacta si el event loop está bloqueado

Timezone

  • Usa el timezone del sistema operativo
  • No soporta timezone específico por tarea
  • Para timezone diferente, ajusta manualmente en el handler

Persistencia

  • No persiste estado entre reinicios
  • No hay historial de ejecuciones (solo contadores)
  • Considera guardar estado en DB si necesitas persistencia

Concurrencia

  • Tareas cron e interval se ejecutan concurrentemente
  • Si un handler tarda mucho, puede superponerse con la siguiente ejecución
  • Usa locks si necesitas exclusión mutua

Número de tareas

  • No hay límite teórico, pero miles de tareas pueden afectar rendimiento
  • Cada tarea cron revisa condición cada segundo
  • Considera agrupar tareas relacionadas

Integración con otros paquetes

Con @jamx-framework/metrics

import { Metrics } from '@jamx-framework/metrics';

scheduler.add({
  name: 'report-generation',
  cron: '0 0 * * *',
  handler: async () => {
    const timer = Metrics.startTimer('scheduler.report.duration');
    try {
      await generateReport();
      Metrics.increment('scheduler.report.success');
    } catch (err) {
      Metrics.increment('scheduler.report.error');
      throw err;
    } finally {
      timer();
    }
  },
});

Con @jamx-framework/queue

import { Queue } from '@jamx-framework/queue';

const queue = new Queue({ driver: 'redis' });

scheduler.add({
  name: 'process-queue',
  every: 10_000,
  handler: async () => {
    const job = await queue.dequeue('email');
    if (job) {
      await sendEmail(job.data);
    }
  },
});

Con @jamx-framework/db

import { Database } from '@jamx-framework/db';

const db = new Database({ driver: 'postgresql', url: process.env.DATABASE_URL! });

scheduler.add({
  name: 'vacuum-database',
  cron: '0 3 * * 0', // Domingo 3:00 AM
  handler: async () => {
    await db.raw('VACUUM ANALYZE');
  },
});

Preguntas frecuentes

¿Cómo ejecutar una tarea solo una vez?

scheduler.add({
  name: 'one-time-task',
  cron: '0 0 * * *',
  runOnInit: true, // se ejecuta al iniciar
  handler: () => {
    console.log('Running once');
  },
});

scheduler.start();
// Después de la primera ejecución, puedes removerla:
// scheduler.remove('one-time-task');

¿Cómo programar tareas dinámicamente?

function scheduleUserReminder(userId: string, when: Date) {
  const taskName = `reminder-${userId}-${when.getTime()}`;

  scheduler.add({
    name: taskName,
    cron: `${when.getMinutes()} ${when.getHours()} * * *`,
    handler: async () => {
      await sendReminder(userId);
      scheduler.remove(taskName); // eliminar después de ejecutar
    },
  });
}

¿Qué pasa si el servidor se reinicia?

  • Todas las tareas se pierden (no hay persistencia)
  • Solución: Usar runOnInit: true para tareas críticas, o re-registrarlas al startup

¿Puedo pausar una tarea sin eliminarla?

No directamente. Debes:

  1. Remover la tarea (remove())
  2. Guardar su definición en otro lugar
  3. Re-añadirla después con add()

¿Cómo manejar dependencias entre tareas?

scheduler.add({
  name: 'task-a',
  cron: '0 * * * *',
  handler: async () => {
    await doA();
  },
});

scheduler.add({
  name: 'task-b',
  cron: '5 * * * *', // 5 minutos después de task-a
  handler: async () => {
    const status = scheduler.taskStatus('task-a');
    if (status?.lastRun && status.lastRun > Date.now() - 10 * 60 * 1000) {
      await doB();
    } else {
      console.log('Skipping task-b: task-a did not run recently');
    }
  },
});

¿Cómo limitar la concurrencia?

import { Semaphore } from '@jamx-framework/core';

const semaphore = new Semaphore(2); // máximo 2 concurrentes

scheduler.add({
  name: 'concurrent-task',
  every: 1000,
  handler: async () => {
    if (!await semaphore.tryAcquire()) {
      console.log('Skipping: too many concurrent executions');
      return;
    }

    try {
      await doWork();
    } finally {
      semaphore.release();
    }
  },
});

Referencia rápida

Crear scheduler

const scheduler = new Scheduler();

Añadir tarea cron

scheduler.add({
  name: 'task-name',
  cron: '0 0 * * *',
  handler: async () => { /* ... */ },
});

Añadir tarea intervalo

scheduler.add({
  name: 'task-name',
  every: 60_000, // ms
  handler: async () => { /* ... */ },
});

Control

scheduler.start();
await scheduler.stop();

Estado

scheduler.statusAll();        // todas las tareas
scheduler.taskStatus('name'); // tarea específica
scheduler.isRunning;          // boolean

Ejecución manual

await scheduler.run('task-name');

Eliminar

scheduler.remove('task-name');

Archivos importantes

  • src/scheduler.ts - Clase Scheduler principal
  • src/cron-parser.ts - Parser de expresiones cron
  • tests/unit/cron-parser.test.ts - Tests del parser
  • tests/unit/scheduler.test.ts - Tests del scheduler

Dependencias

  • @types/node - Tipos de Node.js
  • vitest - Framework de testing
  • rimraf - Limpieza de directorios

Scripts del paquete

  • pnpm build - Compila TypeScript a JavaScript
  • pnpm dev - Compilación en watch mode
  • pnpm test - Ejecuta tests unitarios
  • pnpm test:watch - Tests en watch mode
  • pnpm type-check - Verifica tipos sin compilar
  • pnpm clean - Limpia archivos compilados