package eventbus import "sync" type Event struct { Seq int64 WorkflowRunID string AlbumID string Quality string EventType string Step string Message string Data interface{} } type Subscription struct { Ring *RingBuffer[*Event] C chan struct{} done chan struct{} once sync.Once } type EventBus struct { mu sync.RWMutex topics map[string]map[*Subscription]struct{} global map[*Subscription]struct{} } func New() *EventBus { return &EventBus{ topics: make(map[string]map[*Subscription]struct{}), global: make(map[*Subscription]struct{}), } } func (b *EventBus) Publish(topic string, event *Event) { b.mu.RLock() defer b.mu.RUnlock() if subs, ok := b.topics[topic]; ok { for sub := range subs { sub.Ring.Push(event) select { case sub.C <- struct{}{}: default: } } } for sub := range b.global { sub.Ring.Push(event) select { case sub.C <- struct{}{}: default: } } } func (b *EventBus) Subscribe(topic string) (*Subscription, func()) { sub := &Subscription{ Ring: NewRingBuffer[*Event](256), C: make(chan struct{}, 1), done: make(chan struct{}), } b.mu.Lock() if b.topics[topic] == nil { b.topics[topic] = make(map[*Subscription]struct{}) } b.topics[topic][sub] = struct{}{} b.mu.Unlock() cleanup := func() { sub.once.Do(func() { b.mu.Lock() delete(b.topics[topic], sub) if len(b.topics[topic]) == 0 { delete(b.topics, topic) } b.mu.Unlock() close(sub.done) }) } return sub, cleanup } func (b *EventBus) SubscribeGlobal() (*Subscription, func()) { sub := &Subscription{ Ring: NewRingBuffer[*Event](256), C: make(chan struct{}, 1), done: make(chan struct{}), } b.mu.Lock() b.global[sub] = struct{}{} b.mu.Unlock() cleanup := func() { sub.once.Do(func() { b.mu.Lock() delete(b.global, sub) b.mu.Unlock() close(sub.done) }) } return sub, cleanup } func (b *EventBus) HasTopic(topic string) bool { b.mu.RLock() defer b.mu.RUnlock() _, ok := b.topics[topic] return ok }