> 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/construir/padroes-e-antipadroes/debounce-aggregator.md).

# Debounce / Aggregator

Acumular eventos por uma janela e agir uma vez, em vez de reagir a cada um. No skail, o mecanismo inteiro é um laço de `WhenAny` entre um `WaitForEvent` e um `Delay`, e o estado da acumulação é uma variável local da function.

## Quando usar

Uma sequência de saves enquanto o usuário edita não precisa gerar 50 reindexações; uma rajada de ações em 10 minutos não precisa virar 10 notificações; cinco pedidos do mesmo cliente em meia hora podem sair em uma entrega. Sem o skail, isso é Redis com timer, janelas em stream processing, ou um cron verificando quem acumulou o suficiente, mais uma tabela para o estado. Aqui, o estado é uma lista local que sobrevive à hibernação.

## Debounce: esperar o silêncio

Reindexar o documento só depois que o usuário parar de editar por cinco minutos.

```csharp
public static class Eventos
{
    public const string DocumentoEditado = "DOCUMENTO_EDITADO";
}

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

        if (await SkailTask.WhenAny(edicao, silencio) == silencio)
        {
            await ReindexarDocumento(documentoId);          // command
            return;
        }
        // chegou uma edição antes do silêncio: o laço recomeça com um timer novo
    }
}
```

Cada edição dispara um fire de `DOCUMENTO_EDITADO` com o id do documento. Toda vez que um fire chega antes dos cinco minutos, o laço recomeça com um timer novo; só quando passam cinco minutos inteiros sem evento o `silencio` vence e a reindexação roda. Entre os eventos, a function está hibernada. Cada iteração cria uma espera nova para o mesmo nome e id, e cada fire atende a próxima espera pendente, em ordem.

## Aggregator: acumular o conteúdo

Quando é preciso juntar os payloads para processar em lote, a variável local vira o buffer:

```csharp
public static class Eventos
{
    public const string PedidoProntoParaEnvio = "PEDIDO_PRONTO_ENVIO";
}

[SkailFunction]
public async SkailTask ConsolidarEnvios(Guid clienteId)
{
    var pedidos = new List<Guid>
    {
        await SkailTask.WaitForEvent<Guid>(Eventos.PedidoProntoParaEnvio, clienteId.ToString())   // o primeiro abre a janela
    };

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

        if (await SkailTask.WhenAny(proximo, janela) == janela)
        {
            await CriarEnvioConsolidado(clienteId, pedidos);   // command
            return;
        }

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

A lista `pedidos` é uma variável local, mas a execução é durável: no replay, a function é reexecutada, cada `await proximo` devolve o id gravado no histórico, e a lista é reconstruída exatamente como estava. Se o processo cair e a execução for retomada meia hora depois, os pedidos acumulados estão lá. Nenhuma tabela de pendências, nenhum Redis.

## Limite de lote

Além do silêncio, descarregar assim que o lote atinge um tamanho:

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

    var pedidos = new List<Guid>
    {
        await SkailTask.WaitForEvent<Guid>(Eventos.PedidoProntoParaEnvio, clienteId.ToString())
    };

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

        if (await SkailTask.WhenAny(proximo, janela) == janela) break;

        pedidos.Add(await proximo);
    }

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

O flush acontece pelo que ocorrer primeiro: silêncio de 30 minutos ou lote com 10. A semântica está no código, sem scheduler externo.

## Janela máxima absoluta

Para garantir que, mesmo com eventos chegando sem parar, o lote é descarregado depois de um tempo total, um `Delay` global entra no `WhenAny`:

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

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

    if (await SkailTask.WhenAny(proximo, janela, limiteAbsoluto) != proximo) break;

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

Flush por silêncio de 30 minutos, ou lote cheio, ou duas horas desde o primeiro evento.

## Dois cuidados

A espera que perdeu a corrida fica pendente até a function terminar; um fire que chegue depois do flush não encontra esta execução. Quem dispara precisa tratar a resposta do fire (ou simplesmente iniciar uma nova execução: um novo trigger para `ConsolidarEnvios` abre a próxima janela). E cada iteração adiciona passos ao histórico: para um debounce que pode receber milhares de eventos sem silêncio, use a janela máxima absoluta para encerrar e reiniciar por trigger. Ver [Limites e cotas](/aprender/garantias/limites-e-cotas.md).

## Próximos passos

[Como esperar com timeout](/construir/escrever-fluxos/como-esperar-com-timeout.md) para o mecanismo de `WhenAny`. [Eventos externos e correlação](/aprender/fundamentos/eventos-externos-e-correlacao.md) para múltiplas esperas com o mesmo nome e id. [Régua de lembretes](/construir/padroes-e-antipadroes/regua-de-lembretes.md) para o padrão inverso: emitir em datas, cancelável por evento.
