npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@jamx-framework/scheduler

v1.0.0

Published

JAMX Framework — Cron job scheduler

Downloads

130

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: '[email protected]',
      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