# Server Sent Events

Source: https://docs.sisk-framework.org/ru/docs/features/server-sent-events.html

Sisk поддерживает отправку сообщений через Server Sent Events «из коробки». Вы можете создавать одноразовые и постоянные соединения, получать соединения во время выполнения и использовать их.

Эта возможность имеет некоторые ограничения, налагаемые браузерами, такие как отправка только текстовых сообщений и невозможность полностью закрыть соединение. Соединение, закрытое на стороне сервера, будет периодически пытаться переподключиться клиенту каждые 5 секунд (3 секунды в некоторых браузерах).

Эти соединения полезны для отправки событий с сервера клиенту без необходимости клиенту запрашивать информацию каждый раз.

## Creating an SSE connection

SSE‑соединение работает как обычный HTTP‑запрос, но вместо того, чтобы отправить ответ и сразу закрыть соединение, соединение остаётся открытым для отправки сообщений.

Вызвав метод [HttpRequest.GetEventSource()](https://docs.sisk-framework.org/api/Sisk.Core.Http.HttpRequest.GetEventSource.md), запрос переводится в состояние ожидания, пока создаётся экземпляр SSE.

```cs
r.MapGet("/", (req) =>
{
    using var sse = req.GetEventSource();

    sse.Send("Hello, world!");

    return sse.Close();
});
```

В приведённом выше коде мы создаём SSE‑соединение и отправляем сообщение «Hello, world!», затем закрываем SSE‑соединение со стороны сервера.

> [!NOTE]
> При закрытии соединения на стороне сервера по умолчанию клиент будет пытаться подключиться снова, и соединение будет перезапущено, вызывая метод заново, бесконечно.
>
> Обычно отправляют сообщение о завершении со стороны сервера, когда соединение закрывается, чтобы предотвратить повторные попытки переподключения клиента.

## Appending headers

Если необходимо отправить заголовки, вы можете использовать метод [HttpRequestEventSource.AppendHeader](https://docs.sisk-framework.org/api/Sisk.Core.Http.Streams.HttpRequestEventSource.AppendHeader.md) до отправки любых сообщений.

```cs
r.MapGet("/", (req) =>
{
    using var sse = req.GetEventSource();
    sse.AppendHeader("Header-Key", "Header-value");

    sse.Send("Hello!");

    return sse.Close();
});
```

Обратите внимание, что заголовки нужно отправлять до отправки любых сообщений.

## Wait-For-Fail connections

Соединения обычно завершаются, когда сервер больше не может отправлять сообщения из‑за возможного отключения клиента. При этом соединение автоматически закрывается, а экземпляр класса удаляется.

Даже при переподключении экземпляр класса не будет работать, так как он привязан к предыдущему соединению. В некоторых ситуациях вам может понадобиться это соединение позже, и вы не хотите управлять им через метод‑обратный вызов маршрута.

Для этого мы можем идентифицировать SSE‑соединения с помощью идентификатора и получать их позже, даже вне обратного вызова маршрута. Кроме того, мы помечаем соединение атрибутом [WaitForFail](https://docs.sisk-framework.org/api/Sisk.Core.Http.Streams.HttpRequestEventSource.WaitForFail.md), чтобы не завершать маршрут и не закрывать соединение автоматически.

SSE‑соединение в `WaitForFail` ждёт ошибку отправки, вызванную разрывом, или истечения настроенного времени простоя, прежде чем маршрут возобновится и закроет соединение.

```cs
r.MapGet("/", (req) =>
{
    using var sse = req.GetEventSource("my-index-connection");

    sse.WaitForFail(TimeSpan.FromSeconds(15)); // ждать 15 секунд без сообщений перед завершением соединения

    return sse.Close();
});
```

Вышеприведённый метод создаст соединение, обработает его и будет ждать разъединения или ошибки.

```cs
HttpRequestEventSource? evs = server.EventSources.GetByIdentifier("my-index-connection");
if (evs != null)
{
    // соединение всё ещё активно
    evs.Send("Hello again!");
}
```

И приведённый выше фрагмент попытается найти только что созданное соединение и, если оно существует, отправит в него сообщение.

Все активные серверные соединения, которые идентифицированы, будут доступны в коллекции [HttpServer.EventSources](https://docs.sisk-framework.org/api/Sisk.Core.Http.HttpServer.EventSources.md). Эта коллекция хранит только активные и идентифицированные соединения. Закрытые соединения удаляются из коллекции.

> [!NOTE]
> Важно отметить, что keep alive имеет ограничение, установленное компонентами, которые могут быть подключены к Sisk неконтролируемым образом, например веб‑прокси, HTTP‑ядром или сетевым драйвером, и они закрывают простоящие соединения после определённого периода времени.
>
> Поэтому важно поддерживать соединение открытым, отправляя периодические пинги или увеличивая максимальное время до закрытия соединения. Читайте следующий раздел, чтобы лучше понять отправку периодических пингов.

## Setup connections ping policy

Ping Policy — это автоматический способ отправки периодических сообщений вашему клиенту. Эта функция позволяет серверу понять, что клиент отключился от соединения, без необходимости держать соединение открытым бесконечно.

```cs
[RouteGet("/sse")]
public async Task<HttpResponse> Events(HttpRequest request)
{
    using var sse = await request.GetEventSourceAsync("user-events");
    sse.WithPing(ping =>
    {
        ping.DataMessage = "ping-message";
        ping.Interval = TimeSpan.FromSeconds(5);
        ping.Start();
    });
    
    await sse.WaitForFailAsync(TimeSpan.FromMinutes(10));
    return await sse.CloseAsync();
}
```

В приведённом коде каждые 5 секунд клиенту будет отправляться новое ping‑сообщение. Это поддерживает TCP‑соединение живым и предотвращает его закрытие из‑за бездействия. Кроме того, когда сообщение не удаётся отправить, соединение автоматически закрывается, освобождая ресурсы, использованные соединением.

Используйте [SendAsync](https://docs.sisk-framework.org/api/Sisk.Core.Http.Streams.HttpRequestEventSource.SendAsync.md) и [CloseAsync](https://docs.sisk-framework.org/api/Sisk.Core.Http.Streams.HttpRequestEventSource.CloseAsync.md) в асинхронных маршрутах. Если нужно отбросить накопленные события перед закрытием, вызовите [Cancel](https://docs.sisk-framework.org/api/Sisk.Core.Http.Streams.HttpRequestEventSource.Cancel.md).

## Querying connections

Вы можете искать активные соединения, используя предикат по идентификатору соединения, чтобы, например, выполнить широковещательную рассылку.

```cs
HttpRequestEventSource[] evs = server.EventSources.Find(es => es.StartsWith("my-connection-"));
foreach (HttpRequestEventSource e in evs)
{
    e.Send("Broadcasting to all event sources that starts with 'my-connection-'");
}
```

Также можно использовать метод [All](https://docs.sisk-framework.org/api/Sisk.Core.Http.Streams.HttpEventSourceCollection.All.md) для получения всех активных SSE‑соединений.
