dotnet-cqrs/Svrnty.CQRS.Events.Abstractions/Subscriptions/PersistentSubscription.cs

183 lines
5.3 KiB
C#

using System;
using System.Collections.Generic;
using System.Linq;
namespace Svrnty.CQRS.Events.Abstractions.Subscriptions;
/// <summary>
/// Represents a persistent subscription to correlated events.
/// Survives client disconnection and delivers missed events on reconnect.
/// </summary>
public sealed class PersistentSubscription
{
/// <summary>
/// Unique subscription identifier.
/// </summary>
public required string Id { get; init; }
/// <summary>
/// User/client identifier who owns this subscription.
/// </summary>
public required string SubscriberId { get; init; }
/// <summary>
/// Correlation ID to filter events by.
/// Only events with this correlation ID will be delivered.
/// </summary>
public required string CorrelationId { get; init; }
/// <summary>
/// Event type names the subscriber wants to receive.
/// If empty or null, all events for the correlation are delivered.
/// </summary>
public HashSet<string> EventTypes { get; init; } = new();
/// <summary>
/// Event types that complete/close the subscription.
/// When one of these events is delivered, the subscription is marked as Completed.
/// </summary>
public HashSet<string> TerminalEventTypes { get; init; } = new();
/// <summary>
/// How events should be delivered to the client.
/// </summary>
public DeliveryMode DeliveryMode { get; init; } = DeliveryMode.Immediate;
/// <summary>
/// When the subscription was created.
/// </summary>
public DateTimeOffset CreatedAt { get; init; }
/// <summary>
/// Optional expiration time for the subscription.
/// If set, subscription will be marked as Expired after this time.
/// </summary>
public DateTimeOffset? ExpiresAt { get; init; }
/// <summary>
/// When the subscription completed, expired, or was cancelled.
/// </summary>
public DateTimeOffset? CompletedAt { get; private set; }
/// <summary>
/// Last event sequence number successfully delivered to the client.
/// Used for catch-up on reconnect.
/// </summary>
public long LastDeliveredSequence { get; private set; }
/// <summary>
/// Current status of the subscription.
/// </summary>
public SubscriptionStatus Status { get; private set; } = SubscriptionStatus.Active;
/// <summary>
/// Optional connection ID if client is currently connected.
/// </summary>
public string? ConnectionId { get; set; }
/// <summary>
/// Optional data source ID for client-side routing.
/// </summary>
public string? DataSourceId { get; init; }
/// <summary>
/// Mark a sequence number as successfully delivered.
/// </summary>
public void MarkDelivered(long sequence)
{
if (sequence > LastDeliveredSequence)
{
LastDeliveredSequence = sequence;
}
}
/// <summary>
/// Mark subscription as completed (terminal event received).
/// </summary>
public void Complete()
{
if (Status == SubscriptionStatus.Active || Status == SubscriptionStatus.Paused)
{
Status = SubscriptionStatus.Completed;
CompletedAt = DateTimeOffset.UtcNow;
}
}
/// <summary>
/// Mark subscription as cancelled by user.
/// </summary>
public void Cancel()
{
if (Status == SubscriptionStatus.Active || Status == SubscriptionStatus.Paused)
{
Status = SubscriptionStatus.Cancelled;
CompletedAt = DateTimeOffset.UtcNow;
}
}
/// <summary>
/// Mark subscription as expired (TTL reached).
/// </summary>
public void Expire()
{
if (Status == SubscriptionStatus.Active || Status == SubscriptionStatus.Paused)
{
Status = SubscriptionStatus.Expired;
CompletedAt = DateTimeOffset.UtcNow;
}
}
/// <summary>
/// Pause the subscription (stops event delivery).
/// </summary>
public void Pause()
{
if (Status == SubscriptionStatus.Active)
{
Status = SubscriptionStatus.Paused;
}
}
/// <summary>
/// Resume a paused subscription.
/// </summary>
public void Resume()
{
if (Status == SubscriptionStatus.Paused)
{
Status = SubscriptionStatus.Active;
}
}
/// <summary>
/// Check if the subscription has expired.
/// </summary>
public bool IsExpired => ExpiresAt.HasValue && DateTimeOffset.UtcNow > ExpiresAt.Value;
/// <summary>
/// Check if this event type should be delivered to the subscriber.
/// </summary>
public bool ShouldDeliverEventType(string eventTypeName)
{
// If no filter specified, deliver all events
if (EventTypes == null || EventTypes.Count == 0)
return true;
// Check if event type is in the filter list
return EventTypes.Contains(eventTypeName);
}
/// <summary>
/// Check if this event type is a terminal event.
/// </summary>
public bool IsTerminalEvent(string eventTypeName)
{
return TerminalEventTypes != null && TerminalEventTypes.Contains(eventTypeName);
}
/// <summary>
/// Check if subscription can receive events.
/// </summary>
public bool CanReceiveEvents => Status == SubscriptionStatus.Active;
}