Убрать неиспользуемые using по код-стайлу
Прогон dotnet format (IDE0005) по 4 решениям: удалены лишние using, оставшиеся после миграции namespace (676 файлов).
This commit is contained in:
@@ -1,122 +1,121 @@
|
||||
using Deal.Api.Events;
|
||||
using Deal.Api.Extensions;
|
||||
using Deal.Api.Models;
|
||||
using Deal.Api.Services;
|
||||
using Deal.Api.Dtos;
|
||||
|
||||
namespace Deal.Api.Endpoints;
|
||||
|
||||
/// <summary>
|
||||
/// SSE-поток событий канбана: GET /api/events (Ruling 5; прототип events_routes.py L15–38).
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// Открывает <c>text/event-stream</c> с подпиской на канал тенанта сессии (singleton SseBroker).
|
||||
/// События пишутся по мере поступления; при тишине 15 с отправляется ping-комментарий ": ping" —
|
||||
/// соединение держится (переподключение EventSource, api.js L62–104). Завершение — по отвалу клиента
|
||||
/// (CancellationToken = RequestAborted); отписка — в finally. Без сессии — 401 {detail} (Ruling 10,
|
||||
/// паттерн остальных эндпоинтов). Заголовки: Content-Type text/event-stream, Cache-Control: no-cache,
|
||||
/// X-Accel-Buffering: no (запрет буферизации прокси, иначе ping/события задерживаются).
|
||||
/// </remarks>
|
||||
public static class EventsEndpoint
|
||||
{
|
||||
// Путь потока (роутер events, events_routes.py L12: prefix="/api").
|
||||
private const string EventsPath = "/api/events";
|
||||
|
||||
// OpenAPI-тег группы (в прототипе — роутер events_routes.py).
|
||||
private const string OpenApiTag = "events";
|
||||
|
||||
// Тип контента потока (events_routes.py L32).
|
||||
private const string EventStreamContentType = "text/event-stream";
|
||||
|
||||
// Директива кеширования: поток не кешируется (events_routes.py L34).
|
||||
private const string NoCacheHeaderValue = "no-cache";
|
||||
|
||||
// Отключение буферизации ответа nginx-прокси (events_routes.py L35).
|
||||
private const string NoBufferingHeaderValue = "no";
|
||||
|
||||
// Ping-комментарий: строки протокола SSE, начинающиеся с ':', клиент игнорирует.
|
||||
private const string PingComment = ": ping\n\n";
|
||||
|
||||
// Интервал ping при тишине: держим соединение (events_routes.py L24: timeout=15).
|
||||
private static readonly TimeSpan PingInterval = TimeSpan.FromSeconds(15);
|
||||
|
||||
/// <summary>
|
||||
/// Регистрирует GET /api/events.
|
||||
/// </summary>
|
||||
/// <param name="app">Построитель маршрутов приложения.</param>
|
||||
/// <returns>Построитель маршрутов для цепочки вызовов.</returns>
|
||||
public static IEndpointRouteBuilder MapEventsEndpoint(this IEndpointRouteBuilder app)
|
||||
{
|
||||
app.MapGet(EventsPath, StreamEventsAsync).WithTags(OpenApiTag);
|
||||
return app;
|
||||
}
|
||||
|
||||
// GET /api/events: поток text/event-stream канала тенанта сессии.
|
||||
// context: Контекст запроса (сессия — HttpContext.Items).
|
||||
// ct: Отмена запроса: клиент отвалился — завершаем поток и отписываемся.
|
||||
private static async Task StreamEventsAsync(HttpContext context, CancellationToken ct)
|
||||
{
|
||||
CurrentUser? user = context.GetCurrentUser();
|
||||
if (user is null)
|
||||
{
|
||||
await EndpointResults.Unauthorized(AuthHelpers.UnauthorizedDetail).ExecuteAsync(context);
|
||||
return;
|
||||
}
|
||||
|
||||
SseBroker broker = context.RequestServices.GetRequiredService<SseBroker>();
|
||||
SseSubscription subscription = broker.Subscribe(user.TenantId);
|
||||
try
|
||||
{
|
||||
HttpResponse response = context.Response;
|
||||
response.ContentType = EventStreamContentType;
|
||||
response.Headers["Cache-Control"] = NoCacheHeaderValue;
|
||||
response.Headers["X-Accel-Buffering"] = NoBufferingHeaderValue;
|
||||
|
||||
while (true)
|
||||
{
|
||||
using var pingTimeout = CancellationTokenSource.CreateLinkedTokenSource(ct);
|
||||
pingTimeout.CancelAfter(PingInterval);
|
||||
try
|
||||
{
|
||||
await subscription.Events.WaitToReadAsync(pingTimeout.Token);
|
||||
}
|
||||
catch (OperationCanceledException) when (!ct.IsCancellationRequested)
|
||||
{
|
||||
// Тишина 15 с — ping держит соединение; отмену клиента ловит внешний catch.
|
||||
await WriteFrameAsync(response, PingComment, ct);
|
||||
continue;
|
||||
}
|
||||
|
||||
while (subscription.Events.TryRead(out SseEvent? sseEvent))
|
||||
{
|
||||
await WriteFrameAsync(response, sseEvent.RenderFrame(), ct);
|
||||
}
|
||||
}
|
||||
}
|
||||
catch (OperationCanceledException) when (ct.IsCancellationRequested)
|
||||
{
|
||||
// Клиент закрыл соединение — штатное завершение потока.
|
||||
}
|
||||
catch (IOException)
|
||||
{
|
||||
// Сброс соединения клиентом (закрытая вкладка/обрыв сети): ответ уже не доставить.
|
||||
}
|
||||
finally
|
||||
{
|
||||
broker.Unsubscribe(user.TenantId, subscription.Id);
|
||||
}
|
||||
}
|
||||
|
||||
// Пишет frame в поток ответа и сбрасывает буфер — события уходят сразу (не пачкой).
|
||||
// response: Ответ (stream уже начат).
|
||||
// frame: Frame протокола SSE.
|
||||
// ct: Токен отмены запроса.
|
||||
private static async Task WriteFrameAsync(
|
||||
HttpResponse response,
|
||||
string frame,
|
||||
CancellationToken ct)
|
||||
{
|
||||
await response.WriteAsync(frame, ct);
|
||||
await response.Body.FlushAsync(ct);
|
||||
}
|
||||
}
|
||||
using Deal.Api.Events;
|
||||
using Deal.Api.Extensions;
|
||||
using Deal.Api.Models;
|
||||
using Deal.Api.Services;
|
||||
|
||||
namespace Deal.Api.Endpoints;
|
||||
|
||||
/// <summary>
|
||||
/// SSE-поток событий канбана: GET /api/events (Ruling 5; прототип events_routes.py L15–38).
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// Открывает <c>text/event-stream</c> с подпиской на канал тенанта сессии (singleton SseBroker).
|
||||
/// События пишутся по мере поступления; при тишине 15 с отправляется ping-комментарий ": ping" —
|
||||
/// соединение держится (переподключение EventSource, api.js L62–104). Завершение — по отвалу клиента
|
||||
/// (CancellationToken = RequestAborted); отписка — в finally. Без сессии — 401 {detail} (Ruling 10,
|
||||
/// паттерн остальных эндпоинтов). Заголовки: Content-Type text/event-stream, Cache-Control: no-cache,
|
||||
/// X-Accel-Buffering: no (запрет буферизации прокси, иначе ping/события задерживаются).
|
||||
/// </remarks>
|
||||
public static class EventsEndpoint
|
||||
{
|
||||
// Путь потока (роутер events, events_routes.py L12: prefix="/api").
|
||||
private const string EventsPath = "/api/events";
|
||||
|
||||
// OpenAPI-тег группы (в прототипе — роутер events_routes.py).
|
||||
private const string OpenApiTag = "events";
|
||||
|
||||
// Тип контента потока (events_routes.py L32).
|
||||
private const string EventStreamContentType = "text/event-stream";
|
||||
|
||||
// Директива кеширования: поток не кешируется (events_routes.py L34).
|
||||
private const string NoCacheHeaderValue = "no-cache";
|
||||
|
||||
// Отключение буферизации ответа nginx-прокси (events_routes.py L35).
|
||||
private const string NoBufferingHeaderValue = "no";
|
||||
|
||||
// Ping-комментарий: строки протокола SSE, начинающиеся с ':', клиент игнорирует.
|
||||
private const string PingComment = ": ping\n\n";
|
||||
|
||||
// Интервал ping при тишине: держим соединение (events_routes.py L24: timeout=15).
|
||||
private static readonly TimeSpan PingInterval = TimeSpan.FromSeconds(15);
|
||||
|
||||
/// <summary>
|
||||
/// Регистрирует GET /api/events.
|
||||
/// </summary>
|
||||
/// <param name="app">Построитель маршрутов приложения.</param>
|
||||
/// <returns>Построитель маршрутов для цепочки вызовов.</returns>
|
||||
public static IEndpointRouteBuilder MapEventsEndpoint(this IEndpointRouteBuilder app)
|
||||
{
|
||||
app.MapGet(EventsPath, StreamEventsAsync).WithTags(OpenApiTag);
|
||||
return app;
|
||||
}
|
||||
|
||||
// GET /api/events: поток text/event-stream канала тенанта сессии.
|
||||
// context: Контекст запроса (сессия — HttpContext.Items).
|
||||
// ct: Отмена запроса: клиент отвалился — завершаем поток и отписываемся.
|
||||
private static async Task StreamEventsAsync(HttpContext context, CancellationToken ct)
|
||||
{
|
||||
CurrentUser? user = context.GetCurrentUser();
|
||||
if (user is null)
|
||||
{
|
||||
await EndpointResults.Unauthorized(AuthHelpers.UnauthorizedDetail).ExecuteAsync(context);
|
||||
return;
|
||||
}
|
||||
|
||||
SseBroker broker = context.RequestServices.GetRequiredService<SseBroker>();
|
||||
SseSubscription subscription = broker.Subscribe(user.TenantId);
|
||||
try
|
||||
{
|
||||
HttpResponse response = context.Response;
|
||||
response.ContentType = EventStreamContentType;
|
||||
response.Headers["Cache-Control"] = NoCacheHeaderValue;
|
||||
response.Headers["X-Accel-Buffering"] = NoBufferingHeaderValue;
|
||||
|
||||
while (true)
|
||||
{
|
||||
using var pingTimeout = CancellationTokenSource.CreateLinkedTokenSource(ct);
|
||||
pingTimeout.CancelAfter(PingInterval);
|
||||
try
|
||||
{
|
||||
await subscription.Events.WaitToReadAsync(pingTimeout.Token);
|
||||
}
|
||||
catch (OperationCanceledException) when (!ct.IsCancellationRequested)
|
||||
{
|
||||
// Тишина 15 с — ping держит соединение; отмену клиента ловит внешний catch.
|
||||
await WriteFrameAsync(response, PingComment, ct);
|
||||
continue;
|
||||
}
|
||||
|
||||
while (subscription.Events.TryRead(out SseEvent? sseEvent))
|
||||
{
|
||||
await WriteFrameAsync(response, sseEvent.RenderFrame(), ct);
|
||||
}
|
||||
}
|
||||
}
|
||||
catch (OperationCanceledException) when (ct.IsCancellationRequested)
|
||||
{
|
||||
// Клиент закрыл соединение — штатное завершение потока.
|
||||
}
|
||||
catch (IOException)
|
||||
{
|
||||
// Сброс соединения клиентом (закрытая вкладка/обрыв сети): ответ уже не доставить.
|
||||
}
|
||||
finally
|
||||
{
|
||||
broker.Unsubscribe(user.TenantId, subscription.Id);
|
||||
}
|
||||
}
|
||||
|
||||
// Пишет frame в поток ответа и сбрасывает буфер — события уходят сразу (не пачкой).
|
||||
// response: Ответ (stream уже начат).
|
||||
// frame: Frame протокола SSE.
|
||||
// ct: Токен отмены запроса.
|
||||
private static async Task WriteFrameAsync(
|
||||
HttpResponse response,
|
||||
string frame,
|
||||
CancellationToken ct)
|
||||
{
|
||||
await response.WriteAsync(frame, ct);
|
||||
await response.Body.FlushAsync(ct);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user