Skip to content

Bots, colas y CLI

Las actualizaciones de bots, los trabajos en cola y los comandos de CLI son operaciones asíncronas acotadas. Los ejemplos crean un scope por cada actualización, trabajo o comando y usan await using cuando la función es dueña de toda la operación.

Comparten examples/_shared/container.ts. Compara la unidad de trabajo que cada biblioteca entrega al código de la aplicación.

EjemploMuestra
telegraf.tsScope de actualización de Telegraf
grammy.tsScope de actualización de Grammy
bullmq.tsScope de trabajo de BullMQ
commander.tsScope de comando de Commander
yargs.tsScope de comando de Yargs

Telegraf

ts
import { Telegraf, type Context, type MiddlewareFn } from 'telegraf'

import {
  buildRootContainer,
  createRequestScope,
  type RequestContainer
} from '../_shared/container.js'

const root = buildRootContainer()

type BotContext = Context & {
  container: RequestContainer
}

const withContainer: MiddlewareFn<BotContext> = async (ctx, next) => {
  /*
   * Re-use the shared per-request scope shape: `update_id` becomes the
   * requestId, the Telegram user id becomes the userId. This way the
   * shared services (Logger, AuditService) get the same fields they would
   * see in HTTP request context
   */
  await using scope = createRequestScope(root, {
    requestId: String(ctx.update.update_id),
    userId: ctx.from?.id !== undefined ? String(ctx.from.id) : undefined
  })

  ctx.container = scope
  await next()
}

export const bot = new Telegraf<BotContext>(process.env.BOT_TOKEN!)

bot.use(withContainer)
bot.start(async (ctx) => {
  const users = await ctx.container.getAsync('users')
  const profile = await users.profile(String(ctx.from?.id ?? 'anonymous'))
  await ctx.reply(`Hello ${profile.name}`)
})

Archivo del repositorio: examples/workers-cli/telegraf.ts

Grammy

ts
import { Bot, type Context, type MiddlewareFn } from 'grammy'

import {
  buildRootContainer,
  createRequestScope,
  type RequestContainer
} from '../_shared/container.js'

const root = buildRootContainer()

type BotContext = Context & {
  container: RequestContainer
}

const withContainer: MiddlewareFn<BotContext> = async (ctx, next) => {
  await using scope = createRequestScope(root, {
    requestId: String(ctx.update.update_id),
    userId: ctx.from?.id !== undefined ? String(ctx.from.id) : undefined
  })

  ctx.container = scope
  await next()
}

export const bot = new Bot<BotContext>(process.env.BOT_TOKEN!)

bot.use(withContainer)
bot.command('help', async (ctx) => {
  const users = await ctx.container.getAsync('users')
  const profile = await users.profile(String(ctx.from?.id ?? 'anonymous'))
  await ctx.reply(`Chat profile: ${profile.name}`)
})

Archivo del repositorio: examples/workers-cli/grammy.ts

BullMQ

ts
import { Worker, type Job } from 'bullmq'

import {
  buildRootContainer,
  createRequestScope
} from '../_shared/container.js'

const root = buildRootContainer()

export const worker = new Worker('email', async (job: Job<{ to: string }>) => {
  /*
   * Reuse the same per-request shape for jobs: jobId → requestId, payload
   * recipient → userId so AuditService records pick up the right user
   */
  await using scope = createRequestScope(root, {
    requestId: job.id ?? `job:${job.name}`,
    userId: job.data.to
  })

  scope.get('audit').record('email.sent', { name: job.name, to: job.data.to })
  // Real implementation would call scope.get('mailer').send(job.data)
})

export async function closeWorker() {
  await worker.close()
  await root.dispose()
}

Archivo del repositorio: examples/workers-cli/bullmq.ts

Commander

ts
import { Command } from 'commander'

import { buildRootContainer } from '../_shared/container.js'

const root = buildRootContainer()

export const program = new Command()

program
  .command('import-users <file>')
  .option('--dry-run')
  .action(async (file: string, options: { dryRun?: boolean }) => {
    /* A command invocation owns one operation scope */
    await using scope = root.createScope({
      request: { requestId: `cli:import-users:${Date.now()}` }
    })

    scope.get('audit').record('cli.import-users.start', {
      file,
      dryRun: options.dryRun === true
    })

    /*
     * Real implementation would resolve `users` with `getAsync()` and stream
     * records from `file`. The minimal demo logs the operation lifecycle
     */
    scope.get('audit').record('cli.import-users.done', { file })
  })

export async function run(argv: string[] = process.argv) {
  try {
    await program.parseAsync(argv)
  } finally {
    await root.dispose()
  }
}

Archivo del repositorio: examples/workers-cli/commander.ts

Yargs

ts
import yargs from 'yargs'
import { hideBin } from 'yargs/helpers'

import { buildRootContainer } from '../_shared/container.js'

const root = buildRootContainer()

export const cli = yargs(hideBin(process.argv))
  .command(
    'sync <target>',
    'Sync a target',
    (builder) =>
      builder
        .positional('target', { type: 'string', demandOption: true })
        .option('verbose', { type: 'boolean', default: false }),
    async (argv) => {
      /* Each command invocation owns one operation scope */
      await using scope = root.createScope({
        request: { requestId: `cli:sync:${Date.now()}` }
      })

      scope.get('audit').record('cli.sync', {
        target: argv.target,
        verbose: argv.verbose
      })
    }
  )
  .strict()

export async function run() {
  try {
    await cli.parseAsync()
  } finally {
    await root.dispose()
  }
}

Archivo del repositorio: examples/workers-cli/yargs.ts