Files
silo-server/internal/events/hub.go
T

150 lines
2.8 KiB
Go

package events
import (
"context"
"encoding/json"
"sync"
"time"
"github.com/Silo-Server/silo-server/internal/cache"
"github.com/oklog/ulid/v2"
)
type subscriber struct {
ch chan Envelope
}
// Hub fans out multiplexed events locally and across nodes.
type Hub struct {
mu sync.RWMutex
subscribers map[*subscriber]struct{}
sourceID string
eventBus cache.EventBus
drops map[EventChannel]uint64
}
func NewHub(sourceID string, eventBus cache.EventBus) *Hub {
return &Hub{
subscribers: make(map[*subscriber]struct{}),
sourceID: sourceID,
eventBus: eventBus,
drops: make(map[EventChannel]uint64),
}
}
func (h *Hub) Start(ctx context.Context) error {
if h == nil || h.eventBus == nil || ctx == nil {
return nil
}
return h.eventBus.Subscribe(ctx, cache.ChannelEvents, h.handleEventBusMessage)
}
func (h *Hub) Subscribe() (<-chan Envelope, func()) {
sub := &subscriber{ch: make(chan Envelope, 64)}
h.mu.Lock()
h.subscribers[sub] = struct{}{}
h.mu.Unlock()
return sub.ch, func() {
h.mu.Lock()
if _, ok := h.subscribers[sub]; ok {
delete(h.subscribers, sub)
close(sub.ch)
}
h.mu.Unlock()
}
}
func (h *Hub) Publish(ctx context.Context, env Envelope) error {
if h == nil {
return nil
}
if env.Timestamp.IsZero() {
env.Timestamp = time.Now().UTC()
}
if env.SourceID == "" {
env.SourceID = h.sourceID
}
if env.EventID == "" {
env.EventID = ulid.Make().String()
}
h.publishLocal(env)
if h.eventBus == nil {
return nil
}
payload, err := json.Marshal(env)
if err != nil {
return err
}
return h.eventBus.Publish(ctx, cache.ChannelEvents, cache.Event{
Type: cache.EventEventsNotification,
Payload: string(payload),
})
}
func (h *Hub) PublishJSON(
ctx context.Context,
channel EventChannel,
event string,
data any,
opts PublishOptions,
) error {
var payload json.RawMessage
if data != nil {
encoded, err := json.Marshal(data)
if err != nil {
return err
}
payload = encoded
}
return h.Publish(ctx, Envelope{
Channel: channel,
Event: event,
EventID: opts.EventID,
Data: payload,
UserID: opts.UserID,
ProfileID: opts.ProfileID,
AdminOnly: opts.AdminOnly,
})
}
func (h *Hub) Dropped(channel EventChannel) uint64 {
h.mu.RLock()
defer h.mu.RUnlock()
return h.drops[channel]
}
func (h *Hub) publishLocal(env Envelope) {
h.mu.Lock()
defer h.mu.Unlock()
for sub := range h.subscribers {
select {
case sub.ch <- env:
default:
h.drops[env.Channel]++
}
}
}
func (h *Hub) handleEventBusMessage(event cache.Event) {
if event.Type != cache.EventEventsNotification || event.Payload == "" {
return
}
var env Envelope
if err := json.Unmarshal([]byte(event.Payload), &env); err != nil {
return
}
if env.SourceID != "" && env.SourceID == h.sourceID {
return
}
h.publishLocal(env)
}