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