using System.Collections.Concurrent; using System.Threading.Channels; using Nexus.Api.Models; namespace Nexus.Api.Services; public sealed class LiveUpdateService : ILiveUpdateService { private const int ReplayLimit = 256; private readonly ConcurrentDictionary> _subscribers = new(); private readonly object _historyLock = new(); private readonly Queue _history = new(); private long _sequence; public long CurrentSequence => Interlocked.Read(ref _sequence); public Task SubscribeAsync(long? afterSequence = null, CancellationToken ct = default) { var channel = Channel.CreateUnbounded(new UnboundedChannelOptions { SingleReader = true, SingleWriter = false, AllowSynchronousContinuations = false }); var id = Guid.NewGuid(); _subscribers[id] = channel; var replay = afterSequence.HasValue ? GetReplay(afterSequence.Value) : Array.Empty(); foreach (var envelope in replay) { channel.Writer.TryWrite(envelope); } ct.Register(() => { if (_subscribers.TryRemove(id, out var removed)) { removed.Writer.TryComplete(); } }); return Task.FromResult(new LiveUpdateSubscription { Reader = channel.Reader, StartingSequence = replay.LastOrDefault()?.Sequence ?? CurrentSequence }); } public LiveUpdateEnvelope Publish(string type, object payload, string channel = "dashboard") { var envelope = new LiveUpdateEnvelope( type, DateTimeOffset.UtcNow, payload, Interlocked.Increment(ref _sequence), channel); lock (_historyLock) { _history.Enqueue(envelope); while (_history.Count > ReplayLimit) { _history.Dequeue(); } } foreach (var (id, subscriber) in _subscribers) { if (!subscriber.Writer.TryWrite(envelope) && _subscribers.TryRemove(id, out var removed)) { removed.Writer.TryComplete(); } } return envelope; } private LiveUpdateEnvelope[] GetReplay(long afterSequence) { lock (_historyLock) { return _history.Where(item => item.Sequence > afterSequence).ToArray(); } } }