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;
}
}
}