using System.Runtime.CompilerServices; namespace StellaOps.TaskRunner.Client.Pagination; /// /// Generic paginator for API responses. /// /// Type of items being paginated. public sealed class Paginator { private readonly Func>> _fetchPage; private readonly int _pageSize; /// /// Initializes a new paginator. /// /// Function to fetch a page (offset, limit, cancellationToken) -> page. /// Number of items per page (default: 50). public Paginator( Func>> fetchPage, int pageSize = 50) { _fetchPage = fetchPage ?? throw new ArgumentNullException(nameof(fetchPage)); _pageSize = pageSize > 0 ? pageSize : throw new ArgumentOutOfRangeException(nameof(pageSize)); } /// /// Iterates through all pages asynchronously. /// /// Cancellation token. /// Async enumerable of items. public async IAsyncEnumerable GetAllAsync([EnumeratorCancellation] CancellationToken cancellationToken = default) { var offset = 0; while (true) { var page = await _fetchPage(offset, _pageSize, cancellationToken).ConfigureAwait(false); foreach (var item in page.Items) { yield return item; } if (!page.HasMore || page.Items.Count == 0) { break; } offset += page.Items.Count; } } /// /// Collects all items into a list. /// /// Cancellation token. /// List of all items. public async Task> CollectAsync(CancellationToken cancellationToken = default) { var items = new List(); await foreach (var item in GetAllAsync(cancellationToken).ConfigureAwait(false)) { items.Add(item); } return items; } /// /// Gets a single page. /// /// Page number (1-based). /// Cancellation token. /// Single page response. public Task> GetPageAsync(int pageNumber, CancellationToken cancellationToken = default) { if (pageNumber < 1) { throw new ArgumentOutOfRangeException(nameof(pageNumber), "Page number must be >= 1."); } var offset = (pageNumber - 1) * _pageSize; return _fetchPage(offset, _pageSize, cancellationToken); } } /// /// Paginated response wrapper. /// /// Type of items. public sealed record PagedResponse( IReadOnlyList Items, int TotalCount, bool HasMore) { /// /// Creates an empty page. /// public static PagedResponse Empty { get; } = new([], 0, false); /// /// Current page number (1-based) based on offset and page size. /// public int PageNumber(int offset, int pageSize) => pageSize > 0 ? (offset / pageSize) + 1 : 1; } /// /// Extension methods for creating paginators. /// public static class PaginatorExtensions { /// /// Creates a paginator from a fetch function. /// public static Paginator Paginate( this Func>> fetchPage, int pageSize = 50) => new(fetchPage, pageSize); /// /// Takes the first N items from an async enumerable. /// public static async IAsyncEnumerable TakeAsync( this IAsyncEnumerable source, int count, [EnumeratorCancellation] CancellationToken cancellationToken = default) { ArgumentNullException.ThrowIfNull(source); if (count <= 0) { yield break; } var taken = 0; await foreach (var item in source.WithCancellation(cancellationToken).ConfigureAwait(false)) { yield return item; taken++; if (taken >= count) { break; } } } /// /// Skips the first N items from an async enumerable. /// public static async IAsyncEnumerable SkipAsync( this IAsyncEnumerable source, int count, [EnumeratorCancellation] CancellationToken cancellationToken = default) { ArgumentNullException.ThrowIfNull(source); var skipped = 0; await foreach (var item in source.WithCancellation(cancellationToken).ConfigureAwait(false)) { if (skipped < count) { skipped++; continue; } yield return item; } } }