Rob Watson 1f4a931903
Some checks are pending
ci-build / lint (push) Waiting to run
ci-build / build (push) Blocked by required conditions
ci-build / release (push) Blocked by required conditions
ci-scan / Analyze (go) (push) Waiting to run
ci-scan / Analyze (actions) (push) Waiting to run
fix(app): event ordering
Use a single channel per consumer, instead of one channel per
consumer/event tuple. This ensures that overall ordering of events
remains consistent, and avoids introducing subtle race conditions.
2025-04-25 17:42:49 +02:00

50 lines
966 B
Go

package event
import (
"log/slog"
"sync"
)
const defaultChannelSize = 64
// Bus is an event bus.
type Bus struct {
consumers []chan Event
mu sync.Mutex
logger *slog.Logger
}
// NewBus returns a new event bus.
func NewBus(logger *slog.Logger) *Bus {
return &Bus{
logger: logger,
}
}
// Register registers a consumer for all events.
func (b *Bus) Register() <-chan Event {
b.mu.Lock()
defer b.mu.Unlock()
ch := make(chan Event, defaultChannelSize)
b.consumers = append(b.consumers, ch)
return ch
}
// Send sends an event to all registered consumers.
func (b *Bus) Send(evt Event) {
// The mutex is needed to ensure the backing array of b.consumers cannot be
// modified under our feet. There is probably a more efficient way to do this
// but this should be ok.
b.mu.Lock()
defer b.mu.Unlock()
for _, ch := range b.consumers {
select {
case ch <- evt:
default:
b.logger.Warn("Event dropped", "name", evt.name())
}
}
}