Files
GameList/Infrastructure/StateChangeNotifier.cs

70 lines
2.1 KiB
C#

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);
}