dotnet-cqrs/Svrnty.CQRS.Events.Abstractions/Storage/IReadReceiptStore.cs

125 lines
4.4 KiB
C#

using System;
using Svrnty.CQRS.Events.Abstractions.Storage;
using System.Collections.Generic;
using System.Threading;
using System.Threading.Tasks;
namespace Svrnty.CQRS.Events.Abstractions.Storage;
/// <summary>
/// Store for tracking read receipts (consumer acknowledgments of processed events).
/// </summary>
/// <remarks>
/// <para>
/// <strong>Purpose:</strong>
/// Read receipts provide visibility into consumer progress through event streams.
/// Unlike idempotency (which prevents duplicates), read receipts track progress.
/// </para>
/// <para>
/// <strong>Use Cases:</strong>
/// - Dashboard showing consumer lag/progress
/// - Resuming from last processed position
/// - Monitoring consumer health
/// - Detecting stuck consumers
/// </para>
/// </remarks>
public interface IReadReceiptStore
{
/// <summary>
/// Records that a consumer has successfully processed an event.
/// </summary>
/// <param name="consumerId">The consumer identifier.</param>
/// <param name="streamName">The name of the event stream.</param>
/// <param name="eventId">The unique event identifier.</param>
/// <param name="offset">The event's offset/position in the stream.</param>
/// <param name="acknowledgedAt">When the event was acknowledged.</param>
/// <param name="cancellationToken">Cancellation token.</param>
Task AcknowledgeEventAsync(
string consumerId,
string streamName,
string eventId,
long offset,
DateTimeOffset acknowledgedAt,
CancellationToken cancellationToken = default);
/// <summary>
/// Gets the last acknowledged offset for a consumer on a specific stream.
/// </summary>
/// <param name="consumerId">The consumer identifier.</param>
/// <param name="streamName">The name of the event stream.</param>
/// <param name="cancellationToken">Cancellation token.</param>
/// <returns>The last acknowledged offset, or null if no receipts exist.</returns>
Task<long?> GetLastAcknowledgedOffsetAsync(
string consumerId,
string streamName,
CancellationToken cancellationToken = default);
/// <summary>
/// Gets statistics about a consumer's progress on a stream.
/// </summary>
/// <param name="consumerId">The consumer identifier.</param>
/// <param name="streamName">The name of the event stream.</param>
/// <param name="cancellationToken">Cancellation token.</param>
/// <returns>Consumer progress statistics.</returns>
Task<ConsumerProgress?> GetConsumerProgressAsync(
string consumerId,
string streamName,
CancellationToken cancellationToken = default);
/// <summary>
/// Gets all consumers that are tracking a specific stream.
/// </summary>
/// <param name="streamName">The name of the event stream.</param>
/// <param name="cancellationToken">Cancellation token.</param>
/// <returns>List of consumer IDs tracking this stream.</returns>
Task<IReadOnlyList<string>> GetConsumersForStreamAsync(
string streamName,
CancellationToken cancellationToken = default);
/// <summary>
/// Cleans up old read receipts.
/// </summary>
/// <param name="olderThan">Delete receipts older than this timestamp.</param>
/// <param name="cancellationToken">Cancellation token.</param>
/// <returns>Number of receipts deleted.</returns>
Task<int> CleanupAsync(
DateTimeOffset olderThan,
CancellationToken cancellationToken = default);
}
/// <summary>
/// Represents a consumer's progress on a specific stream.
/// </summary>
public sealed class ConsumerProgress
{
/// <summary>
/// The consumer identifier.
/// </summary>
public required string ConsumerId { get; init; }
/// <summary>
/// The stream name.
/// </summary>
public required string StreamName { get; init; }
/// <summary>
/// The last acknowledged offset.
/// </summary>
public required long LastOffset { get; init; }
/// <summary>
/// When the last event was acknowledged.
/// </summary>
public required DateTimeOffset LastAcknowledgedAt { get; init; }
/// <summary>
/// Total number of events acknowledged.
/// </summary>
public required long TotalAcknowledged { get; init; }
/// <summary>
/// When the consumer first started tracking this stream.
/// </summary>
public DateTimeOffset? FirstAcknowledgedAt { get; init; }
}