using System.Collections.Concurrent; using System.Threading.Channels; using OrderSupportTicket.TestServer.Contracts; namespace OrderSupportTicket.TestServer.Services; /// /// 纯内存 SSE 订阅中心。进程退出后事件和订阅全部丢失。 /// public sealed class TicketEventHub { private readonly ConcurrentDictionary _subscriptions = new(); public int SubscriberCount => _subscriptions.Count; public TicketEventSubscription Subscribe( string purchaseAccount, IReadOnlyCollection appIds) { var id = Guid.NewGuid(); var channel = Channel.CreateUnbounded( new UnboundedChannelOptions { SingleReader = true, SingleWriter = false, AllowSynchronousContinuations = false }); var subscription = new Subscription( purchaseAccount, new HashSet(appIds, StringComparer.OrdinalIgnoreCase), channel); _subscriptions[id] = subscription; return new TicketEventSubscription( channel.Reader, () => _subscriptions.TryRemove(id, out _)); } public void Publish( string purchaseAccount, TicketRealtimeNotificationDto notification) { foreach (var subscription in _subscriptions.Values) { if (!string.Equals( subscription.PurchaseAccount, purchaseAccount, StringComparison.Ordinal)) { continue; } if (!subscription.AppIds.Contains(notification.AppId)) { continue; } subscription.Channel.Writer.TryWrite(notification); } } private sealed record Subscription( string PurchaseAccount, HashSet AppIds, Channel Channel); } public sealed class TicketEventSubscription : IDisposable { private readonly Action _unsubscribe; private int _disposed; internal TicketEventSubscription( ChannelReader reader, Action unsubscribe) { Reader = reader; _unsubscribe = unsubscribe; } public ChannelReader Reader { get; } public void Dispose() { if (Interlocked.Exchange(ref _disposed, 1) != 0) { return; } _unsubscribe(); } }