70 lines
2.1 KiB
C#
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);
|
|
}
|