using System; using System.Collections.Generic; using System.Collections.ObjectModel; using System.Threading; using System.Threading.Tasks; using StellaOps.Notify.Models; namespace StellaOps.Notify.Queue; /// /// Message queued for Notify event processing. /// public sealed class NotifyQueueEventMessage { private readonly NotifyEvent _event; private readonly IReadOnlyDictionary _attributes; public NotifyQueueEventMessage( NotifyEvent @event, string stream, string? idempotencyKey = null, string? partitionKey = null, string? traceId = null, IReadOnlyDictionary? attributes = null) { _event = @event ?? throw new ArgumentNullException(nameof(@event)); if (string.IsNullOrWhiteSpace(stream)) { throw new ArgumentException("Stream must be provided.", nameof(stream)); } Stream = stream; IdempotencyKey = string.IsNullOrWhiteSpace(idempotencyKey) ? @event.EventId.ToString("N") : idempotencyKey!; PartitionKey = string.IsNullOrWhiteSpace(partitionKey) ? null : partitionKey.Trim(); TraceId = string.IsNullOrWhiteSpace(traceId) ? null : traceId.Trim(); _attributes = attributes is null ? EmptyReadOnlyDictionary.Instance : new ReadOnlyDictionary(new Dictionary(attributes, StringComparer.Ordinal)); } public NotifyEvent Event => _event; public string Stream { get; } public string IdempotencyKey { get; } public string TenantId => _event.Tenant; public string? PartitionKey { get; } public string? TraceId { get; } public IReadOnlyDictionary Attributes => _attributes; } /// /// Message queued for channel delivery execution. /// public sealed class NotifyDeliveryQueueMessage { public const string DefaultStream = "notify:deliveries"; private readonly IReadOnlyDictionary _attributes; public NotifyDeliveryQueueMessage( NotifyDelivery delivery, string channelId, NotifyChannelType channelType, string? stream = null, string? traceId = null, IReadOnlyDictionary? attributes = null) { Delivery = delivery ?? throw new ArgumentNullException(nameof(delivery)); ChannelId = NotifyValidation.EnsureNotNullOrWhiteSpace(channelId, nameof(channelId)); ChannelType = channelType; Stream = string.IsNullOrWhiteSpace(stream) ? DefaultStream : stream!.Trim(); TraceId = string.IsNullOrWhiteSpace(traceId) ? null : traceId.Trim(); _attributes = attributes is null ? EmptyReadOnlyDictionary.Instance : new ReadOnlyDictionary(new Dictionary(attributes, StringComparer.Ordinal)); } public NotifyDelivery Delivery { get; } public string ChannelId { get; } public NotifyChannelType ChannelType { get; } public string Stream { get; } public string? TraceId { get; } public string TenantId => Delivery.TenantId; public string IdempotencyKey => Delivery.DeliveryId; public string PartitionKey => ChannelId; public IReadOnlyDictionary Attributes => _attributes; } public readonly record struct NotifyQueueEnqueueResult(string MessageId, bool Deduplicated); public sealed class NotifyQueueLeaseRequest { public NotifyQueueLeaseRequest(string consumer, int batchSize, TimeSpan leaseDuration) { if (string.IsNullOrWhiteSpace(consumer)) { throw new ArgumentException("Consumer must be provided.", nameof(consumer)); } if (batchSize <= 0) { throw new ArgumentOutOfRangeException(nameof(batchSize), batchSize, "Batch size must be positive."); } if (leaseDuration <= TimeSpan.Zero) { throw new ArgumentOutOfRangeException(nameof(leaseDuration), leaseDuration, "Lease duration must be positive."); } Consumer = consumer; BatchSize = batchSize; LeaseDuration = leaseDuration; } public string Consumer { get; } public int BatchSize { get; } public TimeSpan LeaseDuration { get; } } public sealed class NotifyQueueClaimOptions { public NotifyQueueClaimOptions(string claimantConsumer, int batchSize, TimeSpan minIdleTime) { if (string.IsNullOrWhiteSpace(claimantConsumer)) { throw new ArgumentException("Consumer must be provided.", nameof(claimantConsumer)); } if (batchSize <= 0) { throw new ArgumentOutOfRangeException(nameof(batchSize), batchSize, "Batch size must be positive."); } if (minIdleTime < TimeSpan.Zero) { throw new ArgumentOutOfRangeException(nameof(minIdleTime), minIdleTime, "Minimum idle time cannot be negative."); } ClaimantConsumer = claimantConsumer; BatchSize = batchSize; MinIdleTime = minIdleTime; } public string ClaimantConsumer { get; } public int BatchSize { get; } public TimeSpan MinIdleTime { get; } } public enum NotifyQueueReleaseDisposition { Retry, Abandon } public interface INotifyQueue { ValueTask PublishAsync(TMessage message, CancellationToken cancellationToken = default); ValueTask>> LeaseAsync(NotifyQueueLeaseRequest request, CancellationToken cancellationToken = default); ValueTask>> ClaimExpiredAsync(NotifyQueueClaimOptions options, CancellationToken cancellationToken = default); } public interface INotifyQueueLease { string MessageId { get; } int Attempt { get; } DateTimeOffset EnqueuedAt { get; } DateTimeOffset LeaseExpiresAt { get; } string Consumer { get; } string Stream { get; } string TenantId { get; } string? PartitionKey { get; } string IdempotencyKey { get; } string? TraceId { get; } IReadOnlyDictionary Attributes { get; } TMessage Message { get; } Task AcknowledgeAsync(CancellationToken cancellationToken = default); Task RenewAsync(TimeSpan leaseDuration, CancellationToken cancellationToken = default); Task ReleaseAsync(NotifyQueueReleaseDisposition disposition, CancellationToken cancellationToken = default); Task DeadLetterAsync(string reason, CancellationToken cancellationToken = default); } public interface INotifyEventQueue : INotifyQueue { } public interface INotifyDeliveryQueue : INotifyQueue { } internal static class EmptyReadOnlyDictionary where TKey : notnull { public static readonly IReadOnlyDictionary Instance = new ReadOnlyDictionary(new Dictionary(0, EqualityComparer.Default)); }