Add event-driven state sync with ETag optimization
This commit is contained in:
@@ -1,4 +1,5 @@
|
||||
using GameList.Data;
|
||||
using GameList.Domain;
|
||||
using Microsoft.AspNetCore.Authentication;
|
||||
|
||||
namespace GameList.Infrastructure;
|
||||
@@ -10,12 +11,22 @@ public class EnsurePlayerExistsMiddleware(RequestDelegate next)
|
||||
if (context.User.Identity?.IsAuthenticated == true)
|
||||
{
|
||||
var id = context.User.FindFirst(System.Security.Claims.ClaimTypes.NameIdentifier)?.Value;
|
||||
if (string.IsNullOrWhiteSpace(id) || !Guid.TryParse(id, out var playerId) || await db.Players.FindAsync(playerId) is null)
|
||||
if (string.IsNullOrWhiteSpace(id) || !Guid.TryParse(id, out var playerId))
|
||||
{
|
||||
await context.SignOutAsync();
|
||||
context.Response.StatusCode = StatusCodes.Status401Unauthorized;
|
||||
return;
|
||||
}
|
||||
|
||||
var player = await db.Players.FindAsync(playerId);
|
||||
if (player is null)
|
||||
{
|
||||
await context.SignOutAsync();
|
||||
context.Response.StatusCode = StatusCodes.Status401Unauthorized;
|
||||
return;
|
||||
}
|
||||
|
||||
context.Items[nameof(Player)] = player;
|
||||
}
|
||||
|
||||
await next(context);
|
||||
|
||||
31
Infrastructure/StateChangeNotificationMiddleware.cs
Normal file
31
Infrastructure/StateChangeNotificationMiddleware.cs
Normal file
@@ -0,0 +1,31 @@
|
||||
namespace GameList.Infrastructure;
|
||||
|
||||
public sealed class StateChangeNotificationMiddleware(RequestDelegate next)
|
||||
{
|
||||
public async Task InvokeAsync(HttpContext context, StateChangeNotifier notifier)
|
||||
{
|
||||
await next(context);
|
||||
|
||||
if (ShouldNotify(context))
|
||||
notifier.NotifyChange();
|
||||
}
|
||||
|
||||
private static bool ShouldNotify(HttpContext context)
|
||||
{
|
||||
if (context.Response.StatusCode >= StatusCodes.Status400BadRequest)
|
||||
return false;
|
||||
|
||||
if (!HttpMethods.IsPost(context.Request.Method)
|
||||
&& !HttpMethods.IsPut(context.Request.Method)
|
||||
&& !HttpMethods.IsDelete(context.Request.Method))
|
||||
return false;
|
||||
|
||||
var path = context.Request.Path;
|
||||
|
||||
return path.StartsWithSegments("/api/suggestions", StringComparison.OrdinalIgnoreCase)
|
||||
|| path.StartsWithSegments("/api/votes", StringComparison.OrdinalIgnoreCase)
|
||||
|| path.StartsWithSegments("/api/admin", StringComparison.OrdinalIgnoreCase)
|
||||
|| path.StartsWithSegments("/api/me/phase", StringComparison.OrdinalIgnoreCase)
|
||||
|| path.StartsWithSegments("/api/auth/register", StringComparison.OrdinalIgnoreCase);
|
||||
}
|
||||
}
|
||||
69
Infrastructure/StateChangeNotifier.cs
Normal file
69
Infrastructure/StateChangeNotifier.cs
Normal file
@@ -0,0 +1,69 @@
|
||||
using Microsoft.Extensions.Primitives;
|
||||
|
||||
namespace GameList.Infrastructure;
|
||||
|
||||
public sealed class StateChangeNotifier
|
||||
{
|
||||
private readonly string _instanceId = Guid.NewGuid().ToString("N");
|
||||
private long _version = 1;
|
||||
private TaskCompletionSource<long> _nextChange = CreateWaiter();
|
||||
|
||||
public long CurrentVersion => Interlocked.Read(ref _version);
|
||||
|
||||
public string CurrentEtag => $"\"{_instanceId}:{CurrentVersion}\"";
|
||||
|
||||
public long NotifyChange()
|
||||
{
|
||||
var newVersion = Interlocked.Increment(ref _version);
|
||||
|
||||
while (true)
|
||||
{
|
||||
var waiter = Volatile.Read(ref _nextChange);
|
||||
var replacement = CreateWaiter();
|
||||
if (Interlocked.CompareExchange(ref _nextChange, replacement, waiter) != waiter)
|
||||
continue;
|
||||
|
||||
waiter.TrySetResult(newVersion);
|
||||
return newVersion;
|
||||
}
|
||||
}
|
||||
|
||||
public bool MatchesCurrentEtag(StringValues ifNoneMatchValues)
|
||||
{
|
||||
if (StringValues.IsNullOrEmpty(ifNoneMatchValues))
|
||||
return false;
|
||||
|
||||
var current = CurrentEtag;
|
||||
foreach (var raw in ifNoneMatchValues)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(raw))
|
||||
continue;
|
||||
|
||||
var parts = raw.Split(',', StringSplitOptions.TrimEntries | StringSplitOptions.RemoveEmptyEntries);
|
||||
foreach (var part in parts)
|
||||
{
|
||||
if (part == "*" || string.Equals(part, current, StringComparison.Ordinal))
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
public async Task<long> WaitForChangeAsync(long observedVersion, CancellationToken cancellationToken)
|
||||
{
|
||||
while (true)
|
||||
{
|
||||
var current = CurrentVersion;
|
||||
if (current > observedVersion)
|
||||
return current;
|
||||
|
||||
var waiter = Volatile.Read(ref _nextChange);
|
||||
var signaled = await waiter.Task.WaitAsync(cancellationToken);
|
||||
if (signaled > observedVersion)
|
||||
return signaled;
|
||||
}
|
||||
}
|
||||
|
||||
private static TaskCompletionSource<long> CreateWaiter() => new(TaskCreationOptions.RunContinuationsAsynchronously);
|
||||
}
|
||||
Reference in New Issue
Block a user