ts-async-patterns
暂无简介
- License 许可
- MIT
- 所属仓库
- allanpablo/forja
- 最近更新
- 2026-11-15
- Stars
- ★ 2
- Forks
- 0
03 SKILL.md 正文
Padrões Assíncronos em TypeScript
O Node roda seu código numa thread só e faz I/O de forma concorrente. Isso torna a concorrência fácil de começar e fácil de errar: um Promise.all sobre dez mil itens abre dez mil conexões, uma promise esquecida rejeita sem ninguém ouvindo, e um setInterval mantém o processo vivo depois do desligamento. Toda operação assíncrona precisa de um dono que a aguarde e de uma forma de pará-la.
A saída padrão é uma escolha de padrão com a justificativa e um checklist. Escreva código só quando o usuário pedir.
Convenções
- Entradas: metas de vazão e latência de
requirements-gathering/observability, limites de taxa dos serviços a jusante, o código existente. - Os padrões já existentes no código prevalecem. APIs da plataforma primeiro (
AbortController,node:stream/promises,node:timers/promises,node:worker_threads), depois bibliotecas pequenas e focadas (p-limit,p-map,piscina) quando o projeto permite dependências. - Promises soltas são pegas pelo linter (
@typescript-eslint/no-floating-promisesou otypescript/no-floating-promisesdo oxlint); vejats-code-review.
Decida primeiro
A concorrência se justifica quando o trabalho é independente e limitado por I/O (chamadas, consultas, arquivos). Trabalho limitado por CPU não fica mais rápido com mais promises: ele bloqueia o event loop e precisa de worker threads ou de outro processo. "Deixar mais rápido" sem uma medição não é motivo. for … of sequencial com await é o padrão.
Regras
- Posse: a função que inicia o trabalho assíncrono o aguarda ou o entrega a alguém que aguarda. Nada de "dispara e esquece" em caminhos de requisição. Uma tarefa de fundo deliberada passa por uma fila ou por um supervisor que registra e tenta de novo.
- Cancelamento: toda operação longa ou de saída aceita um
AbortSignale o repassa (fetch(url, { signal }), drivers de banco,setTimeoutdenode:timers/promises). Combine sinais comAbortSignal.any([pai, AbortSignal.timeout(ms)]). - Prazos: toda chamada de saída tem um timeout derivado do orçamento de quem chama.
fetchnão tem timeout padrão. - Concorrência limitada: nunca
Promise.all(items.map(fn))sobre uma entrada sem limite. Use um limite (p-limit,p-mapcomconcurrency, ou um pequeno pool de workers). - Erros: toda promise tem um tratamento. Escolha de propósito entre
Promise.all(falha rápido; as outras continuam rodando, a menos que você as aborte) ePromise.allSettled(coleta todos os resultados). Uma rejeição sem tratamento derruba o processo Node por padrão; isso está certo, então não a silencie globalmente. - Estado compartilhado: o event loop torna atômicas as instruções isoladas, não as sequências que atravessam um
await. Um ler-alterar-escrever que atravessa umawaitprecisa de um lock, uma fila ou uma transação. - Recursos: libere em
finallyou comawait using(gerenciamento explícito de recursos) para conexões, handles de arquivo e locks. Remova os listeners e limpe os timers que você criou. - Nunca bloqueie o loop: nada de criptografia ou compressão síncrona,
JSON.parsegrande ou APIs de arquivo*Syncem caminhos de requisição.
Padrões
| Necessidade | Padrão |
|---|---|
| Rodar N tarefas independentes, falhar rápido no primeiro erro | Promise.all com limite, mais um AbortController abortado na primeira falha |
| Rodar N tarefas e reportar todos os resultados | Promise.allSettled com limite; mapeie os resultados por índice |
| Processar um fluxo de jobs com paralelismo fixo | Pool de workers limitado (p-limit / p-map) ou uma fila com N consumidores |
| Transformar dados em etapas | node:stream + pipeline() de node:stream/promises, ou geradores assíncronos consumidos com for await |
| Chamar vários backends e juntar os resultados | Fan-out com limite e AbortSignal.timeout por chamada; colete num array indexado pela posição |
| Trabalho limitado por CPU | worker_threads (um pool como piscina), nunca na thread principal |
| Limitar a taxa de chamadas a uma dependência | Token bucket ou o limitador do provedor; respeite Retry-After |
| Evitar trabalho concorrente duplicado | Memorize a promise em andamento por chave (single-flight) e limpe-a ao concluir |
| Tentar de novo uma falha transitória | Backoff exponencial com jitter e um número máximo de tentativas, só para operações idempotentes |
Backpressure
Quando os produtores são mais rápidos que os consumidores, escolha de propósito:
- Bloquear: respeite o
write()devolvendofalsee espere o'drain', ou usepipeline(), que faz isso por você. Padrão para pipelines internos. - Rejeitar: devolva
429/503quando a fila estiver cheia. Para serviços guiados por requisições. - Descartar ou amostrar: para dados do tipo telemetria, em que a perda é aceitável. Conte os descartes como métrica.
Nunca use um array sem limite como fila: ele transforma sobrecarga em estouro de heap.
Desligamento limpo
Em SIGTERM/SIGINT: pare de aceitar trabalho (server.close(), depois server.closeIdleConnections()), aborte o AbortController raiz, espere o trabalho em andamento com um prazo, descarregue e feche os recursos (pool do banco, consumidores de fila, telemetria) e então saia. Registre os handlers uma vez, na raiz de composição. Os frameworks oferecem ganchos para isso (app.enableShutdownHooks() no NestJS, onClose no Fastify).
Checklist
- [ ] Toda promise é aguardada, devolvida ou explicitamente possuída por um supervisor.
- [ ] Toda operação longa ou de saída recebe um
AbortSignale tem timeout. - [ ] A concorrência sobre coleções é limitada.
- [ ] Grupos que falham rápido abortam o trabalho restante no primeiro erro.
- [ ] Nenhum ler-alterar-escrever de estado compartilhado atravessa um
awaitsem lock ou transação. - [ ] Nenhum trabalho pesado de CPU ou
*Syncno event loop em caminhos de requisição. - [ ] Listeners, intervals e timers criados são removidos no desligamento.
- [ ] O desligamento escoa o trabalho em andamento dentro de um prazo.
- [ ] Os testes usam timers falsos (
vi.useFakeTimers(),mock.timersnonode:test) ou relógios injetados em vez de esperas reais.
Formato
## Problema
<carga, tamanho da entrada, metas, limitado por I/O ou por CPU>
## Decisão
<sequencial | padrão>, porque <motivo>. Limite: <n>, a partir de <medição ou limite a jusante>.
Cancelamento/timeouts: <sinais e orçamentos>. Backpressure: <bloquear | rejeitar | descartar>. Desligamento: <sequência>.
## Checklist
<itens com ✅/❌ para o código existente>
Próximo
ts-testingpara testes com timers falsos e de caminhos de aborto.ts-code-reviewverifica o checklist na implementação.observability: atraso do event loop, profundidade da fila, quantidade em andamento e descartes como métricas.
Evite
Promise.allsobre entrada sem limite.- Callbacks
asyncpassados aoforEach(ninguém os aguarda). - Promises iniciadas e nunca aguardadas em caminhos de requisição.
- Esperas baseadas em
setTimeoutpara sincronização. - Engolir erros com
.catch(() => {}), ou um handler global deunhandledRejectionque só registra. - Trabalho limitado por CPU na thread principal.
- Adicionar concorrência sem uma medição mostrando que ajuda.