183 lines
5.3 KiB
C#
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;
|
|
}
|