using System.Text.Json; using FastEndpoints; namespace SpaceGame.Api.Universe.Api; public sealed class StreamWorldHandler(WorldService worldService) : EndpointWithoutRequest { private static readonly JsonSerializerOptions SseJsonOptions = new(JsonSerializerDefaults.Web); public override void Configure() { Get("/api/world/stream"); AllowAnonymous(); } public override async Task HandleAsync(CancellationToken cancellationToken) { HttpContext.Response.Headers.Append("Cache-Control", "no-cache"); HttpContext.Response.Headers.Append("Content-Type", "text/event-stream"); var afterSequenceRaw = HttpContext.Request.Query["afterSequence"].ToString(); _ = long.TryParse(afterSequenceRaw, out var afterSequence); var scopeKind = HttpContext.Request.Query["scopeKind"].ToString(); if (string.IsNullOrWhiteSpace(scopeKind)) { scopeKind = HttpContext.Request.Query["scope"].ToString(); } if (string.IsNullOrWhiteSpace(scopeKind)) { scopeKind = "universe"; } var systemId = HttpContext.Request.Query["systemId"].ToString(); var bubbleId = HttpContext.Request.Query["bubbleId"].ToString(); var scope = new ObserverScope( scopeKind, string.IsNullOrWhiteSpace(systemId) ? null : systemId, string.IsNullOrWhiteSpace(bubbleId) ? null : bubbleId); var stream = worldService.Subscribe(scope, afterSequence, cancellationToken); await HttpContext.Response.WriteAsync(": connected\n\n", cancellationToken); await HttpContext.Response.Body.FlushAsync(cancellationToken); await foreach (var delta in stream.ReadAllAsync(cancellationToken)) { var payload = JsonSerializer.Serialize(delta, SseJsonOptions); await HttpContext.Response.WriteAsync($"event: world-delta\ndata: {payload}\n\n", cancellationToken); await HttpContext.Response.Body.FlushAsync(cancellationToken); } } }