Skip to content

Comandos Asíncronos

Los comandos asíncronos te permiten ejecutar tareas fuera del flujo principal de la aplicación. Son ideales para operaciones lentas como envío de emails, procesamiento de archivos o integraciones con servicios externos.

Conceptos clave

  • @command('name'): registra una clase como un comando ejecutable.
  • @commandHandler(CommandClass): registra un handler que procesa un comando.
  • ICommandHandler<C>: interfaz que debe implementar cada handler.
  • Async: servicio para ejecutar o programar comandos.
  • Descubrimiento automático: el runner del proyecto detecta tus handlers y activa el procesamiento por ti.

Paso 1: Definir un comando

Un comando es una clase de datos decorada con @command. Cada campo lleva los mismos decoradores de validación que el resto del framework — el comando se valida antes de encolarse:

import { command, isString, isNotEmpty } from '@wabot-dev/framework'
@command('send-email')
export class SendEmailCommand {
@isString()
@isNotEmpty()
to: string = ''
@isString()
@isNotEmpty()
subject: string = ''
@isString()
body: string = ''
}

Paso 2: Crear el handler

El handler implementa ICommandHandler<C> y contiene la lógica de ejecución:

import { commandHandler, ICommandHandler } from '@wabot-dev/framework'
@commandHandler(SendEmailCommand)
export class SendEmailHandler implements ICommandHandler<SendEmailCommand> {
constructor(private emailService: EmailService) {}
async handle(command: SendEmailCommand) {
await this.emailService.send({
to: command.to,
subject: command.subject,
body: command.body,
})
}
}

El handler recibe la instancia ya validada y transformada del comando, con sus campos accesibles directamente.

Paso 3: Ejecutar comandos

Usa el servicio Async para ejecutar o programar comandos:

import { Async, injectable } from '@wabot-dev/framework'
@injectable()
export class NotificationService {
constructor(private async: Async) {}
// Ejecutar inmediatamente (en background)
async sendWelcomeEmail(userEmail: string) {
await this.async.runCommand(SendEmailCommand, {
to: userEmail,
subject: 'Bienvenido',
body: 'Gracias por registrarte.',
})
}
// Programar para después
async scheduleReminder(userEmail: string) {
await this.async.scheduleCommand(
SendEmailCommand,
{
to: userEmail,
subject: 'Recordatorio',
body: 'No olvides completar tu perfil.',
},
{ hours: 24 }, // Ejecutar en 24 horas
)
}
// Programar para una fecha específica
async scheduleReport(adminEmail: string) {
const tomorrow = new Date()
tomorrow.setDate(tomorrow.getDate() + 1)
tomorrow.setHours(9, 0, 0, 0)
await this.async.scheduleCommand(
SendEmailCommand,
{
to: adminEmail,
subject: 'Reporte diario',
body: 'Aquí está tu reporte...',
},
tomorrow, // Ejecutar mañana a las 9:00
)
}
}

Opciones de scheduling

El segundo parámetro de scheduleCommand acepta:

  • Date: fecha exacta de ejecución.
  • { seconds: number }: delay en segundos.
  • { minutes: number }: delay en minutos.
  • { hours: number }: delay en horas.
  • { days: number }: delay en días.

Paso 4: Ciclo de vida

Cada comando pasa por estos estados:

  1. Scheduled: el comando fue registrado y espera ejecución.
  2. Running: el handler está procesando el comando.
  3. Success: el handler terminó sin errores.
  4. Failed: el handler lanzó un error.

Los comandos fallidos se reintentan automáticamente. Puedes ajustar los reintentos por comando configurándolos en el decorador del handler:

@commandHandler({
command: SendEmailCommand,
reintentsDelaysInSeconds: [30, 120, 600], // 3 reintentos: 30s, 2min, 10min
})
export class SendEmailHandler implements ICommandHandler<SendEmailCommand> {
// ...
}

Paso 5: El sistema arranca solo

No necesitas registrar nada: el runner del proyecto (run(config) en src/_run_.ts) descubre las clases decoradas con @commandHandler y activa los workers automáticamente. Con DATABASE_URL configurada los jobs se persisten en PostgreSQL; sin base de datos se procesan en memoria.

Solo si ejecutas fuera del runner necesitas activarlos manualmente con runCommandHandlers([...]) y detenerlos con stopCommandHandlers([...]).

Ejemplo completo

Sistema de notificaciones programadas para un bot de ventas:

import {
command, commandHandler, ICommandHandler,
Async, singleton, isString, isNotEmpty, isIn,
} from '@wabot-dev/framework'
// --- Comando ---
@command('send-notification')
export class SendNotificationCommand {
@isString()
@isNotEmpty()
prospectId: string = ''
@isString()
@isIn(['followup', 'offer_expiring', 'welcome'])
type: 'followup' | 'offer_expiring' | 'welcome' = 'followup'
@isString()
@isNotEmpty()
message: string = ''
}
// --- Handler ---
@commandHandler(SendNotificationCommand)
export class SendNotificationHandler implements ICommandHandler<SendNotificationCommand> {
constructor(
private whatsappService: WhatsAppService,
private prospectRepository: ProspectRepository,
) {}
async handle(command: SendNotificationCommand) {
const prospect = await this.prospectRepository.findById(command.prospectId)
if (!prospect) return
const phone = prospect.contactInfo.find(c => c.type === 'phone')
if (!phone) return
await this.whatsappService.sendMessage(phone.value, command.message)
}
}
// --- Servicio que programa notificaciones ---
@singleton()
export class FollowUpService {
constructor(private async: Async) {}
async scheduleFollowUp(prospectId: string) {
// Enviar seguimiento en 2 horas
await this.async.scheduleCommand(
SendNotificationCommand,
{
prospectId,
type: 'followup',
message: 'Hola, quería saber si tuviste oportunidad de revisar nuestra propuesta.',
},
{ hours: 2 },
)
}
async scheduleOfferExpiry(prospectId: string, expiresAt: Date) {
// Notificar 24 horas antes de que expire la oferta
const notifyAt = new Date(expiresAt.getTime() - 24 * 60 * 60 * 1000)
await this.async.scheduleCommand(
SendNotificationCommand,
{
prospectId,
type: 'offer_expiring',
message: 'Tu oferta especial vence mañana. ¿Te gustaría aprovecharla?',
},
notifyAt,
)
}
async sendWelcome(prospectId: string) {
// Enviar inmediatamente
await this.async.runCommand(SendNotificationCommand, {
prospectId,
type: 'welcome',
message: 'Bienvenido. Soy Martín, tu asesor comercial. ¿En qué puedo ayudarte?',
})
}
}

El comando y su handler quedan registrados al importarse — el runner del proyecto los descubre y activa los workers sin más código.

Siguiente paso

Programa tareas recurrentes con expresiones cron: Tareas Programadas (Cron).