Files
kanban/backend/events.go
T
egutierrez 2524340759 chore: auto-commit (21 archivos)
- app.md
- backend/dist/assets/index-D_Kep7Fb.js
- backend/dist/index.html
- backend/handlers.go
- backend/main.go
- frontend/src/App.tsx
- frontend/src/api.ts
- frontend/src/components/CardChatPanel.tsx
- frontend/src/components/LoginPage.tsx
- frontend/src/types.ts
- ...

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-20 18:17:04 +02:00

159 lines
4.0 KiB
Go

package main
import (
"encoding/json"
"sync"
"sync/atomic"
"time"
)
// EventHub is an in-process pub/sub used to push board mutations and
// notifications to connected clients (SSE for board-wide events, WS for
// per-card chat). Drop policy on slow consumers: best-effort send; if a
// subscriber's buffered channel is full the event is dropped and the
// hub increments dropCount. Clients are expected to reconcile state via
// a full reload when reconnecting.
type EventHub struct {
mu sync.RWMutex
userSubs map[string]map[chan Event]struct{}
cardSubs map[string]map[chan Event]struct{}
dropCount uint64
}
// Event is the envelope broadcast to subscribers.
//
// Type — discriminator (e.g. "card.updated", "message.created").
// CardID — set when payload pertains to a specific card.
// UserID — set for per-user private events (e.g. notifications). Empty
// means broadcast to every user subscriber.
// Payload — arbitrary JSON describing the change.
// TS — RFC3339 timestamp.
type Event struct {
Type string `json:"type"`
CardID string `json:"card_id,omitempty"`
UserID string `json:"user_id,omitempty"`
Payload json.RawMessage `json:"payload,omitempty"`
TS string `json:"ts"`
}
const eventBufSize = 64
func NewEventHub() *EventHub {
return &EventHub{
userSubs: map[string]map[chan Event]struct{}{},
cardSubs: map[string]map[chan Event]struct{}{},
}
}
// SubscribeUser returns a channel that receives every public event plus
// private events targeted at userID. Caller MUST eventually call
// UnsubscribeUser to release resources.
func (h *EventHub) SubscribeUser(userID string) chan Event {
ch := make(chan Event, eventBufSize)
h.mu.Lock()
set, ok := h.userSubs[userID]
if !ok {
set = map[chan Event]struct{}{}
h.userSubs[userID] = set
}
set[ch] = struct{}{}
h.mu.Unlock()
return ch
}
func (h *EventHub) UnsubscribeUser(userID string, ch chan Event) {
h.mu.Lock()
if set, ok := h.userSubs[userID]; ok {
delete(set, ch)
if len(set) == 0 {
delete(h.userSubs, userID)
}
}
h.mu.Unlock()
close(ch)
}
// SubscribeCard returns a channel that receives events scoped to cardID
// (chat messages + typing indicators).
func (h *EventHub) SubscribeCard(cardID string) chan Event {
ch := make(chan Event, eventBufSize)
h.mu.Lock()
set, ok := h.cardSubs[cardID]
if !ok {
set = map[chan Event]struct{}{}
h.cardSubs[cardID] = set
}
set[ch] = struct{}{}
h.mu.Unlock()
return ch
}
func (h *EventHub) UnsubscribeCard(cardID string, ch chan Event) {
h.mu.Lock()
if set, ok := h.cardSubs[cardID]; ok {
delete(set, ch)
if len(set) == 0 {
delete(h.cardSubs, cardID)
}
}
h.mu.Unlock()
close(ch)
}
// Publish delivers ev to every matching subscriber. If ev.UserID is set
// it is delivered ONLY to that user's subscribers; otherwise it fans out
// to all user subscribers. Card subscribers ALWAYS receive events that
// match ev.CardID. Best-effort: full channels are skipped.
func (h *EventHub) Publish(ev Event) {
if ev.TS == "" {
ev.TS = time.Now().UTC().Format(time.RFC3339)
}
h.mu.RLock()
defer h.mu.RUnlock()
deliver := func(ch chan Event) {
select {
case ch <- ev:
default:
atomic.AddUint64(&h.dropCount, 1)
}
}
if ev.UserID != "" {
if set, ok := h.userSubs[ev.UserID]; ok {
for ch := range set {
deliver(ch)
}
}
} else {
for _, set := range h.userSubs {
for ch := range set {
deliver(ch)
}
}
}
if ev.CardID != "" {
if set, ok := h.cardSubs[ev.CardID]; ok {
for ch := range set {
deliver(ch)
}
}
}
}
func (h *EventHub) DropCount() uint64 {
return atomic.LoadUint64(&h.dropCount)
}
// PublishJSON marshals payload and publishes a single Event.
func (h *EventHub) PublishJSON(typ, cardID, userID string, payload interface{}) {
var raw json.RawMessage
if payload != nil {
b, err := json.Marshal(payload)
if err == nil {
raw = b
}
}
h.Publish(Event{Type: typ, CardID: cardID, UserID: userID, Payload: raw})
}