npm package: @jamx-framework/scheduler
Description: JAMX Framework â€” Cron job scheduler
Author: Stiven-21
Latest version: 1.0.0

# @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
```typescript
import { Scheduler } from '@jamx-framework/scheduler';

const scheduler = new Scheduler();
```

### Añadir tareas cron
```typescript
// 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
```typescript
// 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
```typescript
// Iniciar scheduler
scheduler.start();
console.log('Scheduler started');

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

### Ejecutar tareas manualmente
```typescript
// 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
```typescript
const removed = scheduler.remove('old-task');
if (removed) {
  console.log('Task removed');
}
```

## API Reference

### Tipos

#### TaskHandler
```typescript
type TaskHandler = () => void | Promise<void>;
```
Función que se ejecuta cuando la tarea dispara.

#### CronTask
```typescript
interface CronTask {
  name: string;
  cron: string;
  handler: TaskHandler;
  onError?: (err: unknown) => void;
  runOnInit?: boolean;
}
```
Tarea basada en expresión cron.

#### IntervalTask
```typescript
interface IntervalTask {
  name: string;
  every: number; // milisegundos
  handler: TaskHandler;
  onError?: (err: unknown) => void;
  runOnInit?: boolean;
}
```
Tarea basada en intervalo.

#### TaskDefinition
```typescript
type TaskDefinition = CronTask | IntervalTask;
```
Definición de tarea (cron o intervalo).

#### TaskStatus
```typescript
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
```typescript
type SchedulerStatus = "idle" | "running" | "stopped";
```
Estado del scheduler.

### Clase Scheduler

#### Constructor
```typescript
new Scheduler()
```
Crea un scheduler vacío.

#### add()
```typescript
add(definition: TaskDefinition): this
```
Registra una nueva tarea. Lanza error si ya existe una tarea con el mismo nombre.

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

#### remove()
```typescript
remove(name: string): boolean
```
Elimina una tarea. Retorna `true` si la tarea existía y fue eliminada, `false` si no existía.

#### start()
```typescript
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()
```typescript
stop(): void
```
Detiene el scheduler:
- Cancela ticker de cron
- Cancela todos los intervalos
- Cambia estado a "stopped"

#### run()
```typescript
async run(name: string): Promise<void>
```
Ejecuta una tarea manualmente por nombre. Lanza error si la tarea no existe.

#### statusAll()
```typescript
statusAll(): TaskStatus[]
```
Retorna array con el estado de todas las tareas registradas.

#### taskStatus()
```typescript
taskStatus(name: string): TaskStatus | null
```
Retorna el estado de una tarea específica, o `null` si no existe.

#### isRunning
```typescript
readonly isRunning: boolean
```
`true` si el scheduler está en ejecución.

### Funciones de Cron Parser

#### parseCron()
```typescript
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:**
```typescript
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:**
```typescript
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()
```typescript
function matchesCron(expr: CronExpression, date: Date): boolean
```
Verifica si una fecha específica coincide con la expresión cron.

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

#### nextMatch()
```typescript
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:**
```typescript
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
```typescript
const scheduler = new Scheduler();
scheduler.add({ name: 'task1', cron: '0 0 * * *', handler: () => {} });
scheduler.start();
```

**Dentro de `start()`:**
```typescript
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
```typescript
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
```typescript
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
```typescript
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
```typescript
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
```typescript
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
```typescript
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
```typescript
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
```typescript
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
```typescript
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
```typescript
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
```typescript
// ❌ 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
```typescript
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
```typescript
// 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
```typescript
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
```typescript
// 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`
```typescript
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`
```typescript
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`
```typescript
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?
```typescript
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?
```typescript
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?
```typescript
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?
```typescript
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
```typescript
const scheduler = new Scheduler();
```

### Añadir tarea cron
```typescript
scheduler.add({
  name: 'task-name',
  cron: '0 0 * * *',
  handler: async () => { /* ... */ },
});
```

### Añadir tarea intervalo
```typescript
scheduler.add({
  name: 'task-name',
  every: 60_000, // ms
  handler: async () => { /* ... */ },
});
```

### Control
```typescript
scheduler.start();
await scheduler.stop();
```

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

### Ejecución manual
```typescript
await scheduler.run('task-name');
```

### Eliminar
```typescript
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