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 _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 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 CreateWaiter() => new(TaskCreationOptions.RunContinuationsAsynchronously); }