Fix build and code structure improvements. New but essential UI functionality. CI improvements. Documentation improvements. AI module improvements.
This commit is contained in:
@@ -0,0 +1,221 @@
|
||||
// ----------------------------------------------------------------------------
|
||||
// BroadcastNotificationEndpoint
|
||||
//
|
||||
// Demonstrates:
|
||||
// - IRawStellaEndpoint for streaming response
|
||||
// - Bulk notification delivery with progress
|
||||
// - NDJSON streaming format
|
||||
// - Long-running operation with heartbeat
|
||||
// ----------------------------------------------------------------------------
|
||||
|
||||
using System.Text;
|
||||
using System.Text.Json;
|
||||
using Examples.NotificationService.Models;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using StellaOps.Microservice;
|
||||
|
||||
namespace Examples.NotificationService.Endpoints;
|
||||
|
||||
/// <summary>
|
||||
/// Streaming endpoint for broadcasting notifications to multiple recipients.
|
||||
/// Returns progress updates as NDJSON stream.
|
||||
/// </summary>
|
||||
[StellaEndpoint("POST", "/notifications/broadcast", SupportsStreaming = true, TimeoutSeconds = 600)]
|
||||
public sealed class BroadcastNotificationEndpoint : IRawStellaEndpoint
|
||||
{
|
||||
private readonly ILogger<BroadcastNotificationEndpoint> _logger;
|
||||
|
||||
public BroadcastNotificationEndpoint(ILogger<BroadcastNotificationEndpoint> logger)
|
||||
{
|
||||
_logger = logger;
|
||||
}
|
||||
|
||||
public async Task<RawResponse> HandleAsync(
|
||||
RawRequestContext context,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
// Parse the broadcast request from body
|
||||
using var reader = new StreamReader(context.Body, Encoding.UTF8);
|
||||
var requestJson = await reader.ReadToEndAsync(cancellationToken);
|
||||
var request = JsonSerializer.Deserialize<BroadcastRequest>(requestJson, new JsonSerializerOptions
|
||||
{
|
||||
PropertyNameCaseInsensitive = true
|
||||
});
|
||||
|
||||
if (request == null || string.IsNullOrEmpty(request.Title))
|
||||
{
|
||||
return RawResponse.BadRequest("Invalid broadcast request");
|
||||
}
|
||||
|
||||
_logger.LogInformation(
|
||||
"Starting broadcast. Type: {Type}, Recipients: {Count}, CorrelationId: {CorrelationId}",
|
||||
request.Type, request.RecipientIds?.Length ?? 0, context.CorrelationId);
|
||||
|
||||
// Create response stream for NDJSON progress updates
|
||||
var stream = new MemoryStream();
|
||||
var writer = new StreamWriter(stream, Encoding.UTF8, leaveOpen: true);
|
||||
|
||||
var broadcastId = $"BCAST-{DateTime.UtcNow:yyyyMMdd}-{Guid.NewGuid():N}"[..26].ToUpperInvariant();
|
||||
var recipients = request.RecipientIds ?? GenerateSimulatedRecipients(request.RecipientCount ?? 100);
|
||||
var totalRecipients = recipients.Length;
|
||||
var deliveredCount = 0;
|
||||
var failedCount = 0;
|
||||
|
||||
try
|
||||
{
|
||||
// Write initial progress
|
||||
await WriteProgressAsync(writer, new BroadcastProgress
|
||||
{
|
||||
BroadcastId = broadcastId,
|
||||
Status = "started",
|
||||
TotalRecipients = totalRecipients,
|
||||
DeliveredCount = 0,
|
||||
FailedCount = 0,
|
||||
PercentComplete = 0,
|
||||
Timestamp = DateTimeOffset.UtcNow
|
||||
}, cancellationToken);
|
||||
|
||||
// Process recipients in batches
|
||||
const int batchSize = 10;
|
||||
var random = new Random();
|
||||
|
||||
for (var i = 0; i < recipients.Length; i += batchSize)
|
||||
{
|
||||
cancellationToken.ThrowIfCancellationRequested();
|
||||
|
||||
var batch = recipients.Skip(i).Take(batchSize).ToArray();
|
||||
|
||||
// Simulate sending to each recipient in batch
|
||||
foreach (var recipientId in batch)
|
||||
{
|
||||
// Simulate occasional failures (5%)
|
||||
if (random.NextDouble() < 0.05)
|
||||
{
|
||||
failedCount++;
|
||||
_logger.LogDebug("Failed to deliver to {RecipientId}", recipientId);
|
||||
}
|
||||
else
|
||||
{
|
||||
deliveredCount++;
|
||||
}
|
||||
}
|
||||
|
||||
// Simulate processing time
|
||||
await Task.Delay(TimeSpan.FromMilliseconds(50 + random.Next(100)), cancellationToken);
|
||||
|
||||
// Write progress update
|
||||
var percentComplete = (int)((i + batch.Length) * 100.0 / totalRecipients);
|
||||
await WriteProgressAsync(writer, new BroadcastProgress
|
||||
{
|
||||
BroadcastId = broadcastId,
|
||||
Status = "in_progress",
|
||||
TotalRecipients = totalRecipients,
|
||||
DeliveredCount = deliveredCount,
|
||||
FailedCount = failedCount,
|
||||
PercentComplete = percentComplete,
|
||||
CurrentBatch = i / batchSize + 1,
|
||||
TotalBatches = (totalRecipients + batchSize - 1) / batchSize,
|
||||
Timestamp = DateTimeOffset.UtcNow
|
||||
}, cancellationToken);
|
||||
}
|
||||
|
||||
// Write completion
|
||||
await WriteProgressAsync(writer, new BroadcastProgress
|
||||
{
|
||||
BroadcastId = broadcastId,
|
||||
Status = "completed",
|
||||
TotalRecipients = totalRecipients,
|
||||
DeliveredCount = deliveredCount,
|
||||
FailedCount = failedCount,
|
||||
PercentComplete = 100,
|
||||
Timestamp = DateTimeOffset.UtcNow,
|
||||
Summary = new BroadcastSummary
|
||||
{
|
||||
SuccessRate = (decimal)deliveredCount / totalRecipients * 100,
|
||||
Duration = TimeSpan.FromSeconds(totalRecipients * 0.015) // Simulated
|
||||
}
|
||||
}, cancellationToken);
|
||||
|
||||
_logger.LogInformation(
|
||||
"Broadcast {BroadcastId} completed. Delivered: {Delivered}, Failed: {Failed}",
|
||||
broadcastId, deliveredCount, failedCount);
|
||||
}
|
||||
catch (OperationCanceledException)
|
||||
{
|
||||
await WriteProgressAsync(writer, new BroadcastProgress
|
||||
{
|
||||
BroadcastId = broadcastId,
|
||||
Status = "cancelled",
|
||||
TotalRecipients = totalRecipients,
|
||||
DeliveredCount = deliveredCount,
|
||||
FailedCount = failedCount,
|
||||
PercentComplete = (int)(deliveredCount * 100.0 / totalRecipients),
|
||||
Timestamp = DateTimeOffset.UtcNow
|
||||
}, CancellationToken.None);
|
||||
|
||||
_logger.LogWarning("Broadcast {BroadcastId} cancelled", broadcastId);
|
||||
}
|
||||
|
||||
await writer.FlushAsync(CancellationToken.None);
|
||||
stream.Position = 0;
|
||||
|
||||
var headers = new HeaderCollection();
|
||||
headers.Set("Content-Type", "application/x-ndjson");
|
||||
headers.Set("X-Broadcast-Id", broadcastId);
|
||||
|
||||
return new RawResponse
|
||||
{
|
||||
StatusCode = 200,
|
||||
Headers = headers,
|
||||
Body = stream
|
||||
};
|
||||
}
|
||||
|
||||
private static async Task WriteProgressAsync(StreamWriter writer, BroadcastProgress progress, CancellationToken cancellationToken)
|
||||
{
|
||||
var json = JsonSerializer.Serialize(progress, new JsonSerializerOptions
|
||||
{
|
||||
PropertyNamingPolicy = JsonNamingPolicy.CamelCase
|
||||
});
|
||||
await writer.WriteLineAsync(json);
|
||||
await writer.FlushAsync(cancellationToken);
|
||||
}
|
||||
|
||||
private static string[] GenerateSimulatedRecipients(int count)
|
||||
{
|
||||
return Enumerable.Range(1, count)
|
||||
.Select(i => $"USR-{i:D6}")
|
||||
.ToArray();
|
||||
}
|
||||
|
||||
private sealed record BroadcastRequest
|
||||
{
|
||||
public required string Type { get; init; }
|
||||
public required string Title { get; init; }
|
||||
public required string Body { get; init; }
|
||||
public NotificationPriority Priority { get; init; } = NotificationPriority.Normal;
|
||||
public DeliveryChannel[] Channels { get; init; } = [DeliveryChannel.InApp];
|
||||
public string[]? RecipientIds { get; init; }
|
||||
public int? RecipientCount { get; init; }
|
||||
}
|
||||
|
||||
private sealed record BroadcastProgress
|
||||
{
|
||||
public required string BroadcastId { get; init; }
|
||||
public required string Status { get; init; }
|
||||
public int TotalRecipients { get; init; }
|
||||
public int DeliveredCount { get; init; }
|
||||
public int FailedCount { get; init; }
|
||||
public int PercentComplete { get; init; }
|
||||
public int? CurrentBatch { get; init; }
|
||||
public int? TotalBatches { get; init; }
|
||||
public required DateTimeOffset Timestamp { get; init; }
|
||||
public BroadcastSummary? Summary { get; init; }
|
||||
}
|
||||
|
||||
private sealed record BroadcastSummary
|
||||
{
|
||||
public decimal SuccessRate { get; init; }
|
||||
public TimeSpan Duration { get; init; }
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user