Skip to content

Боты, очереди и CLI

Обновления ботов, задания в очередях и CLI-команды являются ограниченными асинхронными операциями. Примеры создают один scope на обновление, задание или команду и используют await using, когда функция владеет всей операцией.

Они используют общий examples/_shared/container.ts. Сравнивайте единицу работы, которую библиотека передаёт коду приложения.

ПримерЧто показывает
telegraf.tsscope на обновление в Telegraf
grammy.tsscope на обновление в Grammy
bullmq.tsscope на задание в BullMQ
commander.tsscope на команду в Commander
yargs.tsscope на команду в 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 = await 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 profile = await ctx.container.get('users').profile(String(ctx.from?.id ?? 'anonymous'))
  await ctx.reply(`Hello ${profile.name}`)
})

Файл в репозитории: 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 = await 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 profile = await ctx.container.get('users').profile(String(ctx.from?.id ?? 'anonymous'))
  await ctx.reply(`Chat profile: ${profile.name}`)
})

Файл в репозитории: 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 = await 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).
})

Файл в репозитории: 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 CLI invocation is a bounded async unit: `await using` ties scope
    // disposal to the action function's exit (success or throw), so the
    // async `Database` factory in the shared root is closed before the
    // process exits.
    await using scope = root.createScope()
    const ctx = scope.get('request')
    ctx.requestId = `cli:import-users:${Date.now()}`

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

    // Real implementation would stream `file` through scope.get('users')
    // and write to scope.get('db'). The minimal demo just logs.
    scope.get('audit').record('cli.import-users.done', { file })
  })

Файл в репозитории: 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 CLI invocation owns a fresh scope; `await using` disposes the
      // shared `Database` factory deterministically before the action
      // resolves (and therefore before the Node process exits).
      await using scope = root.createScope()
      scope.get('request').requestId = `cli:sync:${Date.now()}`

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

Файл в репозитории: examples/workers-cli/yargs.ts