multica/server/internal/events/bus.go
Naiyuan Qing 9236674667 feat(realtime): WS invalidation + refetch pattern, inbox bugfixes, UI polish
Refactor real-time sync from per-event precise mutations to WS-as-invalidation-signal + debounced refetch.

Backend:
- Add SubscribeAll to Event Bus — auto-broadcasts ALL events, eliminates manual 25-item allEvents list
- Add skill event constants to protocol, fix skill handler string literals
- Add title_changed activity tracking

Frontend:
- WSClient: add onAny() method for wildcard event subscription
- useRealtimeSync: rewrite to refreshMap + prefix routing + 100ms debounce
- Precise handlers only for side effects: workspace:deleted, member:removed, member:added (self-check)
- Reconnect now refetches all stores (fixes missing members/skills/workspace refresh)
- Stale-while-revalidate: fetch() only shows loading spinner on initial load, not on refetch
- Remove redundant useWSEvent in agents/page.tsx and skills-page.tsx
- WSClient.disconnect() now clears all handler registrations

Inbox bugfixes:
- Unify sidebar badge count with page count via dedupedItems + unreadCount in store
- Sort by time DESC (removed severity-first ordering)
- Ellipsis on truncated detail labels

UI:
- Status/Priority pickers: replace RadioGroup with MenuItem for auto-close on selection

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-29 13:49:40 +08:00

81 lines
2.1 KiB
Go

package events
import (
"log/slog"
"sync"
)
// Event represents a domain event published by handlers or services.
type Event struct {
Type string // e.g. "issue:created", "inbox:new"
WorkspaceID string // routes to correct Hub room
ActorType string // "member", "agent", or "system"
ActorID string
Payload any // JSON-serializable, same shape as current WS payloads
}
// Handler is a function that processes an event.
type Handler func(Event)
// Bus is an in-process synchronous pub/sub event bus.
type Bus struct {
mu sync.RWMutex
listeners map[string][]Handler
globalHandlers []Handler
}
// New creates a new event bus.
func New() *Bus {
return &Bus{
listeners: make(map[string][]Handler),
}
}
// Subscribe registers a handler for a given event type.
// Handlers are called synchronously in registration order.
func (b *Bus) Subscribe(eventType string, h Handler) {
b.mu.Lock()
defer b.mu.Unlock()
b.listeners[eventType] = append(b.listeners[eventType], h)
}
// SubscribeAll registers a handler that receives ALL events regardless of type.
// Global handlers are called after type-specific handlers.
func (b *Bus) SubscribeAll(h Handler) {
b.mu.Lock()
defer b.mu.Unlock()
b.globalHandlers = append(b.globalHandlers, h)
}
// Publish dispatches an event to all registered handlers for that event type.
// Type-specific handlers run first, then global (SubscribeAll) handlers.
// Each handler is called synchronously. Panics in individual handlers are
// recovered so one failing handler does not prevent others from executing.
func (b *Bus) Publish(e Event) {
b.mu.RLock()
handlers := b.listeners[e.Type]
globals := b.globalHandlers
b.mu.RUnlock()
for _, h := range handlers {
func() {
defer func() {
if r := recover(); r != nil {
slog.Error("panic in event listener", "event_type", e.Type, "recovered", r)
}
}()
h(e)
}()
}
for _, h := range globals {
func() {
defer func() {
if r := recover(); r != nil {
slog.Error("panic in global event listener", "event_type", e.Type, "recovered", r)
}
}()
h(e)
}()
}
}