using StellaOps.Notify.Models;
namespace StellaOps.Notifier.Worker.Channels;
///
/// Contract implemented by channel adapters to dispatch notifications.
///
public interface IChannelAdapter
{
///
/// Channel type handled by this adapter.
///
NotifyChannelType ChannelType { get; }
///
/// Dispatches a notification delivery through the channel.
///
Task DispatchAsync(
ChannelDispatchContext context,
CancellationToken cancellationToken);
///
/// Checks channel health/connectivity.
///
Task CheckHealthAsync(
NotifyChannel channel,
CancellationToken cancellationToken);
}
///
/// Context for dispatching a notification through a channel.
///
public sealed record ChannelDispatchContext(
string DeliveryId,
string TenantId,
NotifyChannel Channel,
NotifyDelivery Delivery,
string RenderedBody,
string? Subject,
IReadOnlyDictionary Metadata,
DateTimeOffset Timestamp,
string TraceId);
///
/// Result of a channel dispatch attempt.
///
public sealed record ChannelDispatchResult
{
public required bool Success { get; init; }
public required ChannelDispatchStatus Status { get; init; }
public string? Message { get; init; }
public string? ExternalId { get; init; }
public int? HttpStatusCode { get; init; }
public TimeSpan? Duration { get; init; }
public IReadOnlyDictionary Metadata { get; init; } = new Dictionary();
public Exception? Exception { get; init; }
public static ChannelDispatchResult Succeeded(
string? externalId = null,
string? message = null,
TimeSpan? duration = null,
IReadOnlyDictionary? metadata = null) => new()
{
Success = true,
Status = ChannelDispatchStatus.Sent,
ExternalId = externalId,
Message = message ?? "Delivery dispatched successfully.",
Duration = duration,
Metadata = metadata ?? new Dictionary()
};
public static ChannelDispatchResult Failed(
string message,
ChannelDispatchStatus status = ChannelDispatchStatus.Failed,
int? httpStatusCode = null,
Exception? exception = null,
TimeSpan? duration = null,
IReadOnlyDictionary? metadata = null) => new()
{
Success = false,
Status = status,
Message = message,
HttpStatusCode = httpStatusCode,
Exception = exception,
Duration = duration,
Metadata = metadata ?? new Dictionary()
};
public static ChannelDispatchResult Throttled(
string message,
TimeSpan? retryAfter = null,
IReadOnlyDictionary? metadata = null)
{
var meta = metadata is not null
? new Dictionary(metadata)
: new Dictionary();
if (retryAfter.HasValue)
{
meta["retryAfterSeconds"] = retryAfter.Value.TotalSeconds.ToString("F0");
}
return new()
{
Success = false,
Status = ChannelDispatchStatus.Throttled,
Message = message,
HttpStatusCode = 429,
Metadata = meta
};
}
}
///
/// Dispatch attempt status.
///
public enum ChannelDispatchStatus
{
Sent,
Failed,
Throttled,
InvalidConfiguration,
Timeout,
NetworkError
}
///
/// Result of a channel health check.
///
public sealed record ChannelHealthCheckResult
{
public required bool Healthy { get; init; }
public required string Status { get; init; }
public string? Message { get; init; }
public TimeSpan? Latency { get; init; }
public IReadOnlyDictionary Metadata { get; init; } = new Dictionary();
public static ChannelHealthCheckResult Ok(string? message = null, TimeSpan? latency = null) => new()
{
Healthy = true,
Status = "healthy",
Message = message ?? "Channel is operational.",
Latency = latency
};
public static ChannelHealthCheckResult Degraded(string message, TimeSpan? latency = null) => new()
{
Healthy = true,
Status = "degraded",
Message = message,
Latency = latency
};
public static ChannelHealthCheckResult Unhealthy(string message) => new()
{
Healthy = false,
Status = "unhealthy",
Message = message
};
}