> For the complete documentation index, see [llms.txt](https://docs.skail.dev/llms.txt). Markdown versions of documentation pages are available by appending `.md` to page URLs; this page is available as [Markdown](https://docs.skail.dev/quickstart/design-patterns/debounce-aggregator.md).

# Debounce / Aggregator

Muitos eventos que entram no seu sistema não valem um processamento individual. Uma sequência de saves enquanto o usuário edita um documento não precisa gerar 50 re-indexações; uma rajada de ações dentro de 10 minutos não precisa virar 10 push notifications separados; cinco pedidos do mesmo cliente em meia hora podem sair numa única entrega consolidada.

O padrão é antigo e conhecido: em vez de reagir a cada evento, acumule por uma janela e aja uma vez. A parte difícil, historicamente, é o mecanismo — Redis + timer, Kafka Streams com janelas, cron rodando de tempos em tempos para verificar quem acumulou o suficiente, tabela intermediária para guardar o estado da acumulação.

Em skail, toda essa infraestrutura vira um laço de `WhenAny` entre um `WaitForEvent` e um `Delay`. O estado da acumulação é só uma variável local do método.

### Debounce: esperar o silêncio

O caso mais simples. Cada vez que o usuário edita um documento, você *poderia* disparar a re-indexação imediatamente — mas é melhor esperar até que ele pare de editar por 5 minutos antes de gastar esse esforço.

```csharp
[SkailFunction]
public async SkailTask ReindexarAposEdicao(Guid documentoId)
{
    while (true)
    {
        var edicao = SkailTask.WaitForEvent("DOCUMENTO_EDITADO", documentoId.ToString());
        var silencio = SkailTask.Delay(TimeSpan.FromMinutes(5));

        await SkailTask.WhenAny(edicao, silencio);

        if (silencio.IsCompleted)
        {
            await ReindexarDocumento(documentoId);
            return;
        }

        // nova edição chegou — o laço recomeça com um timer novo
    }
}
```

Toda vez que uma edição chega antes do silêncio de 5 minutos, o laço recomeça com um timer novo. Só quando passam 5 minutos inteiros sem evento é que o `silencio` vence e a re-indexação roda. Entre os eventos, a função está hibernada — sem consumir recursos enquanto acumula.

### Aggregator: acumulando conteúdo

Quando você precisa não só esperar o silêncio, mas também juntar os payloads dos eventos para processá-los em batch, a variável local do método vira o buffer da acumulação:

```csharp
[SkailFunction]
public async SkailTask ConsolidarEnvios(Guid clienteId)
{
    var pedidos = new List<Guid>();
    pedidos.Add(await SkailTask.WaitForEvent<Guid>("PEDIDO_PRONTO_ENVIO", clienteId.ToString()));

    while (true)
    {
        var proximo = SkailTask.WaitForEvent<Guid>("PEDIDO_PRONTO_ENVIO", clienteId.ToString());
        var janela = SkailTask.Delay(TimeSpan.FromMinutes(30));

        await SkailTask.WhenAny(proximo, janela);

        if (!proximo.IsCompleted)
        {
            await CriarEnvioConsolidado(clienteId, pedidos);
            return;
        }

        pedidos.Add(await proximo);
    }
}
```

A lista `pedidos` é só uma variável local — mas como a execução é durável, ela persiste junto com a função. Se o processo cair e a função for retomada meia hora depois, a lista volta a existir exatamente no estado em que estava antes da queda. Nenhuma tabela de "pendências de consolidação", nenhum Redis.

### Limite de lote

Em muitos cenários você não quer esperar só o silêncio — quer também flushar assim que o lote alcança um tamanho razoável. Isso é só uma condição extra no laço:

```csharp
[SkailFunction]
public async SkailTask ConsolidarEnvios(Guid clienteId)
{
    const int LimiteLote = 10;

    var pedidos = new List<Guid>();
    pedidos.Add(await SkailTask.WaitForEvent<Guid>("PEDIDO_PRONTO_ENVIO", clienteId.ToString()));

    while (pedidos.Count < LimiteLote)
    {
        var proximo = SkailTask.WaitForEvent<Guid>("PEDIDO_PRONTO_ENVIO", clienteId.ToString());
        var janela = SkailTask.Delay(TimeSpan.FromMinutes(30));

        await SkailTask.WhenAny(proximo, janela);

        if (!proximo.IsCompleted) break;

        pedidos.Add(await proximo);
    }

    await CriarEnvioConsolidado(clienteId, pedidos);
}
```

O flush acontece pelo que ocorrer primeiro: silêncio de 30 minutos, ou lote cheio com 10 pedidos. A semântica fica explícita no código, sem dependência de scheduler externo e sem precisar reconstruir o estado a partir de uma tabela de filas.

### Variação útil: janela máxima absoluta

Outra guarda comum é garantir que, mesmo que os eventos continuem chegando sem parar, o lote é flushado depois de um tempo total. Isso evita que uma rajada contínua deixe o processamento esperando indefinidamente. Adiciona-se um `Delay` global no `WhenAny`:

```csharp
var limiteAbsoluto = SkailTask.Delay(TimeSpan.FromHours(2));

while (pedidos.Count < LimiteLote)
{
    var proximo = SkailTask.WaitForEvent<Guid>("PEDIDO_PRONTO_ENVIO", clienteId.ToString());
    var janela = SkailTask.Delay(TimeSpan.FromMinutes(30));

    await SkailTask.WhenAny(proximo, janela, limiteAbsoluto);

    if (limiteAbsoluto.IsCompleted || !proximo.IsCompleted) break;

    pedidos.Add(await proximo);
}
```

A condição de flush agora é *silêncio de 30 min* ∨ *lote cheio* ∨ *2 horas totais desde o primeiro evento*.

### Observabilidade

No Monitor do portal do skail, uma execução em debounce ou aggregator aparece entre evento e evento, com o timer da janela. Você consegue ver quanto tempo falta para o flush, quantos eventos já foram acumulados até agora (pelo histórico de retomadas), e, quando o flush roda, o comando de agregação executado com a lista final.

Isso dá uma visibilidade que normalmente não existe em soluções baseadas em Redis + cron: é possível abrir uma execução específica e saber exatamente quais eventos ela consumiu, quando cada um chegou, e por qual critério (silêncio, lote cheio ou limite absoluto) o flush foi disparado.
