< Summary

Information
Class: OzonOrdersWeb.WebServices.Orders.Grid.OrdersGridChangesRequest
Assembly: OzonOrdersWeb
File(s): /home/runner/work/Studio2Prod/Studio2Prod/OzonOrdersWeb/WebServices/Orders/Grid/OrdersGridChangesService.cs
Line coverage
100%
Covered lines: 4
Uncovered lines: 0
Coverable lines: 4
Total lines: 152
Line coverage: 100%
Branch coverage
N/A
Covered branches: 0
Total branches: 0
Branch coverage: N/A
Method coverage

Feature is only available for sponsors

Upgrade to PRO version

Metrics

MethodBranch coverage Crap Score Cyclomatic complexity Line coverage
get_Since()100%11100%
get_KnownIds()100%11100%
get_MaxKnownId()100%11100%
get_IncludeArchived()100%11100%

File(s)

/home/runner/work/Studio2Prod/Studio2Prod/OzonOrdersWeb/WebServices/Orders/Grid/OrdersGridChangesService.cs

#LineLine coverage
 1using System.Security.Claims;
 2using Microsoft.EntityFrameworkCore;
 3using OzonRepositories.Context;
 4
 5namespace OzonOrdersWeb.WebServices.Orders.Grid;
 6
 7/// <summary>Запрос дельты: что изменилось после <see cref="Since"/> среди загруженных клиентом строк.</summary>
 8public sealed class OrdersGridChangesRequest
 9{
 10    /// <summary>Серверное время прошлого ответа (<see cref="OrdersGridChangesResult.Now"/>).</summary>
 511    public DateTime? Since { get; set; }
 12
 13    /// <summary>Id строк, которые сейчас в кэше грида (≤ MaxKnownIds).</summary>
 414    public List<int>? KnownIds { get; set; } = [];
 15
 16    /// <summary>Максимальный известный клиенту Id — всё, что больше, считается новыми заказами.</summary>
 117    public int MaxKnownId { get; set; }
 18
 119    public bool IncludeArchived { get; set; }
 20}
 21
 22public sealed class OrdersGridChangesResult
 23{
 24    /// <summary>Серверное время «сейчас» — клиент передаст его как Since в следующий раз.</summary>
 25    public DateTime Now { get; set; }
 26
 27    /// <summary>Since слишком старый или клиент прислал слишком много строк — надо перезагрузить список целиком.</summa
 28    public bool FullRefresh { get; set; }
 29
 30    /// <summary>Изменённые заказы из числа известных клиенту — готовые строки грида.</summary>
 31    public List<OrderGridRowDto> Updated { get; set; } = [];
 32
 33    /// <summary>Известные клиенту заказы, которые изменились, но не поместились в лимит гидрации (клиент перезапросит б
 34    public List<int> UpdatedOverflowIds { get; set; } = [];
 35
 36    /// <summary>Заказов с Id больше MaxKnownId (новые).</summary>
 37    public int NewCount { get; set; }
 38
 39    /// <summary>Изменившихся заказов, которых нет среди известных клиенту (вне загруженных блоков).</summary>
 40    public int ChangedOutsideCount { get; set; }
 41}
 42
 43public interface IOrdersGridChangesService
 44{
 45    Task<OrdersGridChangesResult> GetChangesAsync(OrdersGridChangesRequest request, ClaimsPrincipal principal, Cancellat
 46}
 47
 48/// <summary>
 49/// Дельта для автообновления грида: один индексный запрос по <c>Orders.UpdatedAt</c> (проставляется
 50/// BEFORE-триггером на любой INSERT/UPDATE), разбиение изменённых Id на «известные клиенту» / «новые» /
 51/// «вне экрана» и гидрация только известных через тот же конвейер, что и блоки списка.
 52/// </summary>
 53public sealed class OrdersGridChangesService : IOrdersGridChangesService
 54{
 55    public const int MaxKnownIds = 1000;
 56    public const int MaxHydrate = 50;
 57    /// <summary>Больше изменений за опрос — дешевле перезагрузить список, чем разбирать дельту.</summary>
 58    public const int MaxChanges = 500;
 59    public static readonly TimeSpan MaxSinceAge = TimeSpan.FromMinutes(10);
 60
 61    private readonly ApplicationDbContext _context;
 62    private readonly IOrdersGridRowHydrator _hydrator;
 63    private readonly IOrdersGridScopedQuery _scoped;
 64
 65    public OrdersGridChangesService(ApplicationDbContext context, IOrdersGridRowHydrator hydrator, IOrdersGridScopedQuer
 66    {
 67        _context = context;
 68        _hydrator = hydrator;
 69        _scoped = scoped;
 70    }
 71
 72    public async Task<OrdersGridChangesResult> GetChangesAsync(
 73        OrdersGridChangesRequest request, ClaimsPrincipal principal, CancellationToken cancellationToken)
 74    {
 75        // Время берём из БД: триггер пишет NOW(6) сервера MariaDB, а не веб-сервера.
 76        var now = await _context.Database
 77            .SqlQueryRaw<DateTime>("SELECT NOW(6) AS Value")
 78            .SingleAsync(cancellationToken);
 79
 80        var result = new OrdersGridChangesResult { Now = now };
 81        var knownIds = request.KnownIds ?? [];
 82
 83        if (request.Since is null || now - request.Since.Value > MaxSinceAge || knownIds.Count > MaxKnownIds)
 84        {
 85            result.FullRefresh = true;
 86            return result;
 87        }
 88
 89        var user = await _scoped.ResolveUserAsync(principal);
 90        if (user is null)
 91        {
 92            result.FullRefresh = true;
 93            return result;
 94        }
 95
 96        // Только доступные пользователю заказы (RLS, профиль, архив) и не больше MaxChanges + 1:
 97        // по индексу IX_Orders_UpdatedAt (InnoDB хранит в нём и Id — запрос покрывающий).
 98        var since = request.Since.Value;
 99        var scoped = await _scoped.BuildAsync(user, request.IncludeArchived, cancellationToken);
 100        var changed = await scoped
 101            .Where(o => o.UpdatedAt != null && o.UpdatedAt > since)
 102            .OrderBy(o => o.UpdatedAt)
 103            .Select(o => o.Id)
 104            .Take(MaxChanges + 1)
 105            .ToListAsync(cancellationToken);
 106
 107        if (changed.Count == 0)
 108        {
 109            return result;
 110        }
 111
 112        if (changed.Count > MaxChanges)
 113        {
 114            result.FullRefresh = true;
 115            return result;
 116        }
 117
 118        var known = new HashSet<int>(knownIds);
 119        var split = SplitChanges(changed, known, request.MaxKnownId);
 120        result.NewCount = split.NewIds.Count;
 121        result.ChangedOutsideCount = split.OutsideIds.Count;
 122
 123        if (split.KnownIds.Count == 0)
 124        {
 125            return result;
 126        }
 127
 128        var toHydrate = split.KnownIds.Take(MaxHydrate).ToList();
 129        result.UpdatedOverflowIds = split.KnownIds.Skip(MaxHydrate).ToList();
 130
 131        result.Updated = await _hydrator.HydrateAsync(toHydrate, principal, cancellationToken);
 132
 133        return result;
 134    }
 135
 136    /// <summary>Чистое разбиение изменённых Id: известные клиенту / новые (Id &gt; max) / прочие вне экрана.</summary>
 137    public static (List<int> KnownIds, List<int> NewIds, List<int> OutsideIds) SplitChanges(
 138        IReadOnlyList<int> changedIds, IReadOnlySet<int> knownIds, int maxKnownId)
 139    {
 140        var known = new List<int>();
 141        var fresh = new List<int>();
 142        var outside = new List<int>();
 143        foreach (var id in changedIds)
 144        {
 145            if (knownIds.Contains(id)) known.Add(id);
 146            else if (id > maxKnownId) fresh.Add(id);
 147            else outside.Add(id);
 148        }
 149
 150        return (known, fresh, outside);
 151    }
 152}