diff --git a/apps/backend/Handlers/GetWorldHandler.cs b/apps/backend/Handlers/GetWorldHandler.cs new file mode 100644 index 0000000..366690b --- /dev/null +++ b/apps/backend/Handlers/GetWorldHandler.cs @@ -0,0 +1,16 @@ +using FastEndpoints; +using SpaceGame.Simulation.Api.Simulation; + +namespace SpaceGame.Simulation.Api.Handlers; + +public sealed class GetWorldHandler(WorldService worldService) : EndpointWithoutRequest +{ + public override void Configure() + { + Get("/api/world"); + AllowAnonymous(); + } + + public override Task HandleAsync(CancellationToken cancellationToken) => + SendOkAsync(worldService.GetSnapshot(), cancellationToken); +} diff --git a/apps/backend/Handlers/GetWorldHealthHandler.cs b/apps/backend/Handlers/GetWorldHealthHandler.cs new file mode 100644 index 0000000..7a4f02e --- /dev/null +++ b/apps/backend/Handlers/GetWorldHealthHandler.cs @@ -0,0 +1,24 @@ +using FastEndpoints; +using SpaceGame.Simulation.Api.Simulation; + +namespace SpaceGame.Simulation.Api.Handlers; + +public sealed class GetWorldHealthHandler(WorldService worldService) : EndpointWithoutRequest +{ + public override void Configure() + { + Get("/api/world/health"); + AllowAnonymous(); + } + + public override Task HandleAsync(CancellationToken cancellationToken) + { + var status = worldService.GetStatus(); + return SendOkAsync(new + { + ok = true, + sequence = status.Sequence, + generatedAtUtc = status.GeneratedAtUtc, + }, cancellationToken); + } +} diff --git a/apps/backend/Handlers/ResetWorldHandler.cs b/apps/backend/Handlers/ResetWorldHandler.cs new file mode 100644 index 0000000..f37f369 --- /dev/null +++ b/apps/backend/Handlers/ResetWorldHandler.cs @@ -0,0 +1,16 @@ +using FastEndpoints; +using SpaceGame.Simulation.Api.Simulation; + +namespace SpaceGame.Simulation.Api.Handlers; + +public sealed class ResetWorldHandler(WorldService worldService) : EndpointWithoutRequest +{ + public override void Configure() + { + Post("/api/world/reset"); + AllowAnonymous(); + } + + public override Task HandleAsync(CancellationToken cancellationToken) => + SendOkAsync(worldService.Reset(), cancellationToken); +} diff --git a/apps/backend/Handlers/RootRedirectHandler.cs b/apps/backend/Handlers/RootRedirectHandler.cs new file mode 100644 index 0000000..68cb42c --- /dev/null +++ b/apps/backend/Handlers/RootRedirectHandler.cs @@ -0,0 +1,18 @@ +using FastEndpoints; + +namespace SpaceGame.Simulation.Api.Handlers; + +public sealed class RootRedirectHandler : EndpointWithoutRequest +{ + public override void Configure() + { + Get("/"); + AllowAnonymous(); + } + + public override Task HandleAsync(CancellationToken cancellationToken) + { + HttpContext.Response.Redirect("/api/world"); + return Task.CompletedTask; + } +} diff --git a/apps/backend/Handlers/StreamWorldHandler.cs b/apps/backend/Handlers/StreamWorldHandler.cs new file mode 100644 index 0000000..a8c4fa2 --- /dev/null +++ b/apps/backend/Handlers/StreamWorldHandler.cs @@ -0,0 +1,55 @@ +using System.Text.Json; +using FastEndpoints; +using SpaceGame.Simulation.Api.Contracts; +using SpaceGame.Simulation.Api.Simulation; + +namespace SpaceGame.Simulation.Api.Handlers; + +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); + } + } +} diff --git a/apps/backend/Program.cs b/apps/backend/Program.cs index 199e2f9..ad35ba2 100644 --- a/apps/backend/Program.cs +++ b/apps/backend/Program.cs @@ -1,9 +1,7 @@ -using SpaceGame.Simulation.Api.Contracts; +using FastEndpoints; using SpaceGame.Simulation.Api.Simulation; -using System.Text.Json; var builder = WebApplication.CreateBuilder(args); -var sseJsonOptions = new JsonSerializerOptions(JsonSerializerDefaults.Web); builder.WebHost.UseUrls("http://127.0.0.1:5079"); builder.Services.AddCors((options) => @@ -18,61 +16,13 @@ builder.Services.AddCors((options) => }); builder.Services.Configure(builder.Configuration.GetSection("WorldGeneration")); builder.Services.Configure(builder.Configuration.GetSection("OrbitalSimulation")); +builder.Services.AddFastEndpoints(); builder.Services.AddSingleton(); builder.Services.AddHostedService(); var app = builder.Build(); app.UseCors(); - -app.MapGet("/", () => Results.Redirect("/api/world")); -app.MapGet("/api/world", (WorldService worldService) => Results.Ok(worldService.GetSnapshot())); -app.MapGet("/api/world/stream", async (HttpContext httpContext, WorldService worldService, 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); - } -}); -app.MapGet("/api/world/health", (WorldService worldService) => Results.Ok(new -{ - ok = true, - sequence = worldService.GetStatus().Sequence, - generatedAtUtc = worldService.GetStatus().GeneratedAtUtc, -})); -app.MapPost("/api/world/reset", (WorldService worldService) => -{ - var snapshot = worldService.Reset(); - return Results.Ok(snapshot); -}); +app.UseFastEndpoints(); app.Run(); diff --git a/apps/backend/SpaceGame.Simulation.Api.csproj b/apps/backend/SpaceGame.Simulation.Api.csproj index a3a34b6..fb4b086 100644 --- a/apps/backend/SpaceGame.Simulation.Api.csproj +++ b/apps/backend/SpaceGame.Simulation.Api.csproj @@ -6,4 +6,8 @@ enable + + + +