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:
- Scheduled: el comando fue registrado y espera ejecución.
- Running: el handler está procesando el comando.
- Success: el handler terminó sin errores.
- 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).