package filemanagerrealtime import ( "errors" "log/slog" "strings" "sync" ) const ( FileStatusChangedEventType = "file_status_changed" defaultSubscriberBuffer = 32 ) type FileStatusEvent struct { EventType string `json:"event_type"` FileUUID string `json:"file_uuid"` FolderUUID *string `json:"folder_uuid,omitempty"` Status string `json:"status"` Name string `json:"name"` MimeType string `json:"mime_type,omitempty"` FailureReason *string `json:"failure_reason"` UpdatedAt string `json:"updated_at"` } type Subscription struct { events <-chan FileStatusEvent done <-chan struct{} closeFn func() } func (s *Subscription) Events() <-chan FileStatusEvent { if s == nil { return nil } return s.events } func (s *Subscription) Done() <-chan struct{} { if s == nil { return nil } return s.done } func (s *Subscription) Close() { if s == nil || s.closeFn == nil { return } s.closeFn() } type statusSubscriber struct { events chan FileStatusEvent done chan struct{} match func(FileStatusEvent) bool closeOnce sync.Once } func (s *statusSubscriber) close() { if s == nil { return } s.closeOnce.Do(func() { close(s.done) }) } type Hub struct { mu sync.RWMutex subscribers map[string]map[uint64]*statusSubscriber recentByUser map[string][]FileStatusEvent nextID uint64 buffer int replayLimit int logger *slog.Logger } func NewHub(buffer int, logger *slog.Logger) *Hub { if buffer <= 0 { buffer = defaultSubscriberBuffer } if logger == nil { logger = slog.Default() } return &Hub{ subscribers: make(map[string]map[uint64]*statusSubscriber), recentByUser: make(map[string][]FileStatusEvent), buffer: buffer, replayLimit: maxInt(buffer*4, defaultSubscriberBuffer), logger: logger.With("component", "file-status-hub"), } } func (h *Hub) Subscribe(userUUID string) (*Subscription, error) { return h.SubscribeWithFilter(userUUID, nil) } func (h *Hub) SubscribeWithFilter(userUUID string, match func(FileStatusEvent) bool) (*Subscription, error) { if h == nil { return nil, errors.New("status hub is not configured") } userUUID = canonicalUserID(userUUID) if userUUID == "" { return nil, errors.New("user uuid is required") } sub := &statusSubscriber{ events: make(chan FileStatusEvent, h.buffer), done: make(chan struct{}), match: match, } h.mu.Lock() h.nextID++ subID := h.nextID if h.subscribers[userUUID] == nil { h.subscribers[userUUID] = make(map[uint64]*statusSubscriber) } h.subscribers[userUUID][subID] = sub backlog := append([]FileStatusEvent(nil), h.recentByUser[userUUID]...) h.mu.Unlock() for i := range backlog { if sub.match != nil && !sub.match(backlog[i]) { continue } enqueueSubscriberEvent(sub, backlog[i]) } var closeOnce sync.Once closeFn := func() { closeOnce.Do(func() { h.mu.Lock() defer h.mu.Unlock() if byUser, ok := h.subscribers[userUUID]; ok { delete(byUser, subID) if len(byUser) == 0 { delete(h.subscribers, userUUID) } } sub.close() }) } return &Subscription{ events: sub.events, done: sub.done, closeFn: closeFn, }, nil } func (h *Hub) PublishToUsers(userUUIDs []string, event FileStatusEvent) int { if h == nil { return 0 } if strings.TrimSpace(event.EventType) == "" { event.EventType = FileStatusChangedEventType } if strings.TrimSpace(event.FileUUID) == "" || strings.TrimSpace(event.Status) == "" { return 0 } uniqueUsers := make(map[string]struct{}, len(userUUIDs)) for i := range userUUIDs { userID := canonicalUserID(userUUIDs[i]) if userID == "" { continue } uniqueUsers[userID] = struct{}{} } if len(uniqueUsers) == 0 { return 0 } delivered := 0 for userID := range uniqueUsers { h.mu.Lock() h.recentByUser[userID] = appendRecentEvents(h.recentByUser[userID], event, h.replayLimit) h.mu.Unlock() h.mu.RLock() byUser := h.subscribers[userID] if len(byUser) == 0 { h.mu.RUnlock() continue } for _, sub := range byUser { select { case <-sub.done: continue default: } if sub.match != nil && !sub.match(event) { continue } if enqueueSubscriberEvent(sub, event) { delivered++ } } h.mu.RUnlock() } return delivered } func (h *Hub) SubscriberCount(userUUID string) int { if h == nil { return 0 } userUUID = canonicalUserID(userUUID) if userUUID == "" { return 0 } h.mu.RLock() defer h.mu.RUnlock() return len(h.subscribers[userUUID]) } func canonicalUserID(raw string) string { return strings.ToLower(strings.TrimSpace(raw)) } func enqueueSubscriberEvent(sub *statusSubscriber, event FileStatusEvent) bool { if sub == nil { return false } select { case <-sub.done: return false default: } select { case sub.events <- event: return true default: // Drop the oldest buffered event to prioritize fresh status changes. select { case <-sub.events: default: } select { case sub.events <- event: return true default: return false } } } func appendRecentEvents(current []FileStatusEvent, event FileStatusEvent, limit int) []FileStatusEvent { if limit <= 0 { return current[:0] } current = append(current, event) if len(current) <= limit { return current } overflow := len(current) - limit copy(current, current[overflow:]) return current[:limit] } func maxInt(a, b int) int { if a >= b { return a } return b }