Add event-driven state sync with ETag optimization
This commit is contained in:
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