Bots, files et CLI
Les mises à jour de bots, jobs de file et commandes CLI sont des opérations asynchrones délimitées. Les exemples créent un scope par mise à jour, job ou commande et utilisent await using lorsque la fonction gère l’opération complète.
Ils utilisent le graphe partagé. Compare l’unité de travail confiée au code de l’application par chaque bibliothèque. examples/_shared/container.ts
| Exemple | Contenu |
|---|---|
telegraf.ts | Scope de mise à jour Telegraf |
grammy.ts | Scope de mise à jour Grammy |
bullmq.ts | Scope de job BullMQ |
commander.ts | Scope de commande Commander |
yargs.ts | Scope de commande 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}`)
})Fichier du dépôt : 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}`)
})Fichier du dépôt : 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()
}Fichier du dépôt : 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()
}
}Fichier du dépôt : 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()
}
}Fichier du dépôt : examples/workers-cli/yargs.ts
