117 lines
4.5 KiB
C#
117 lines
4.5 KiB
C#
using GameList.Data;
|
|
using GameList.Infrastructure;
|
|
|
|
namespace GameList.Endpoints;
|
|
|
|
public static class StateEndpoints
|
|
{
|
|
public static void MapStateEndpoints(this IEndpointRouteBuilder app)
|
|
{
|
|
var group = app.MapGroup("/api").RequireAuthorization();
|
|
|
|
group.MapGet("/state", async (HttpContext ctx, AppDbContext db, StateWorkflowService service, StateChangeNotifier notifier) =>
|
|
{
|
|
ctx.Response.Headers.CacheControl = "private, no-cache";
|
|
if (notifier.MatchesCurrentEtag(ctx.Request.Headers.IfNoneMatch))
|
|
{
|
|
ctx.Response.Headers.ETag = notifier.CurrentEtag;
|
|
return Results.StatusCode(StatusCodes.Status304NotModified);
|
|
}
|
|
|
|
var player = await EndpointHelpers.GetAuthenticatedPlayer(ctx, db);
|
|
if (player is null)
|
|
return EndpointHelpers.UnauthorizedError();
|
|
|
|
var result = await service.GetStateAsync(player);
|
|
return result.ToHttpResult(payload =>
|
|
{
|
|
ctx.Response.Headers.ETag = notifier.CurrentEtag;
|
|
return Results.Ok(payload);
|
|
});
|
|
});
|
|
|
|
group.MapGet("/events/state", async (HttpContext ctx, AppDbContext db, StateChangeNotifier notifier) =>
|
|
{
|
|
var player = await EndpointHelpers.GetAuthenticatedPlayer(ctx, db);
|
|
if (player is null)
|
|
return EndpointHelpers.UnauthorizedError();
|
|
|
|
ctx.Response.ContentType = "text/event-stream";
|
|
ctx.Response.Headers.CacheControl = "no-cache";
|
|
ctx.Response.Headers["X-Accel-Buffering"] = "no";
|
|
|
|
var observedVersion = notifier.CurrentVersion;
|
|
await WriteStateEventAsync(ctx, "ready", observedVersion, ctx.RequestAborted);
|
|
|
|
while (!ctx.RequestAborted.IsCancellationRequested)
|
|
{
|
|
try
|
|
{
|
|
var changeTask = notifier.WaitForChangeAsync(observedVersion, ctx.RequestAborted);
|
|
var heartbeatTask = Task.Delay(TimeSpan.FromSeconds(20), ctx.RequestAborted);
|
|
var completed = await Task.WhenAny(changeTask, heartbeatTask);
|
|
|
|
if (completed == changeTask)
|
|
{
|
|
observedVersion = await changeTask;
|
|
await WriteStateEventAsync(ctx, "state", observedVersion, ctx.RequestAborted);
|
|
}
|
|
else
|
|
{
|
|
await ctx.Response.WriteAsync(": ping\n\n", ctx.RequestAborted);
|
|
await ctx.Response.Body.FlushAsync(ctx.RequestAborted);
|
|
}
|
|
}
|
|
catch (OperationCanceledException)
|
|
{
|
|
break;
|
|
}
|
|
catch (IOException)
|
|
{
|
|
break;
|
|
}
|
|
}
|
|
|
|
return Results.Empty;
|
|
});
|
|
|
|
group.MapGet("/me", async (HttpContext ctx, AppDbContext db, StateWorkflowService service) =>
|
|
{
|
|
var player = await EndpointHelpers.GetAuthenticatedPlayer(ctx, db);
|
|
if (player is null)
|
|
return EndpointHelpers.UnauthorizedError();
|
|
|
|
var result = await service.GetMeAsync(player);
|
|
return result.ToHttpResult(Results.Ok);
|
|
});
|
|
|
|
group.MapPost("/me/phase/next", async (HttpContext ctx, AppDbContext db, StateWorkflowService service) =>
|
|
{
|
|
var player = await EndpointHelpers.GetAuthenticatedPlayer(ctx, db);
|
|
if (player is null)
|
|
return EndpointHelpers.UnauthorizedError();
|
|
|
|
var result = await service.NextPhaseAsync(player);
|
|
return result.ToHttpResult(Results.Ok);
|
|
});
|
|
|
|
group.MapPost("/me/phase/prev", async (HttpContext ctx, AppDbContext db, StateWorkflowService service) =>
|
|
{
|
|
var player = await EndpointHelpers.GetAuthenticatedPlayer(ctx, db);
|
|
if (player is null)
|
|
return EndpointHelpers.UnauthorizedError();
|
|
|
|
var result = await service.PrevPhaseAsync(player);
|
|
return result.ToHttpResult(Results.Ok);
|
|
});
|
|
|
|
}
|
|
|
|
private static async Task WriteStateEventAsync(HttpContext ctx, string eventName, long version, CancellationToken cancellationToken)
|
|
{
|
|
await ctx.Response.WriteAsync($"event: {eventName}\n", cancellationToken);
|
|
await ctx.Response.WriteAsync($"data: {version}\n\n", cancellationToken);
|
|
await ctx.Response.Body.FlushAsync(cancellationToken);
|
|
}
|
|
}
|