using System.Collections.Concurrent; using System.Threading.Channels; namespace w4c_workflows.Services; /// /// Server-push hub for tenant-scoped realtime events (SSE), mirroring w4c-webapi's hub. A connected /// client gets its own bounded channel; writes a tiny notification into every /// channel for the tenant. The client decides what to re-fetch, keeping broadcasts cheap. The channel /// is bounded and drops the oldest notification when a client cannot keep up, so one stalled SSE /// consumer cannot grow server memory without limit (the next notification makes it re-fetch anyway). /// /// Used to make the frontend an optional view: when a workflow file is written server-side (e.g. by /// the AI transform), every subscribed client in the tenant is notified and reloads the file. /// public sealed class RealtimeEventHub { /// Per-client notification backlog ceiling. private const int ClientChannelCapacity = 256; private readonly ConcurrentDictionary>> _clients = new(); public (Guid Id, ChannelReader Reader) AddClient(string tenantId) { var channel = Channel.CreateBounded(new BoundedChannelOptions(ClientChannelCapacity) { SingleReader = true, SingleWriter = false, FullMode = BoundedChannelFullMode.DropOldest, }); var id = Guid.NewGuid(); var clients = _clients.GetOrAdd(tenantId, _ => new ConcurrentDictionary>()); clients[id] = channel; return (id, channel.Reader); } public void RemoveClient(string tenantId, Guid id) { if (!_clients.TryGetValue(tenantId, out var clients)) return; clients.TryRemove(id, out _); if (clients.IsEmpty) _clients.TryRemove(tenantId, out _); } public void Publish(string tenantId, string kind, string id, DateTime at) { if (!_clients.TryGetValue(tenantId, out var clients)) return; var evt = new RealtimeEvent(kind, id, at); foreach (var channel in clients.Values) { channel.Writer.TryWrite(evt); } } } /// A tenant-scoped change notification: Kind is a component name (e.g. "workflow"), /// Id is that component's server id/path, At is the server-side write time. public record RealtimeEvent(string Kind, string Id, DateTime At);