Боты, очереди и CLI
Обновления ботов, задания в очередях и CLI-команды являются ограниченными асинхронными операциями. Примеры создают один scope на обновление, задание или команду и используют await using, когда функция владеет всей операцией.
Они используют общий examples/_shared/container.ts. Сравнивайте единицу работы, которую библиотека передаёт коду приложения.
| Пример | Что показывает |
|---|---|
telegraf.ts | scope на обновление в Telegraf |
grammy.ts | scope на обновление в Grammy |
bullmq.ts | scope на задание в BullMQ |
commander.ts | scope на команду в Commander |
yargs.ts | scope на команду в 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
