Files
kandev/apps/backend/internal/gateway/websocket/run_notifications.go
T
2026-05-16 20:07:57 +01:00

86 lines
2.4 KiB
Go

package websocket
import (
"context"
"github.com/kandev/kandev/internal/common/logger"
"github.com/kandev/kandev/internal/events"
"github.com/kandev/kandev/internal/events/bus"
ws "github.com/kandev/kandev/pkg/websocket"
"go.uber.org/zap"
)
// RunEventBroadcaster subscribes to office_run_events bus notifications
// using a per-subject wildcard, then fans the event out to every WS
// client subscribed to that specific run id via run.subscribe.
//
// Why a wildcard rather than per-run subscriptions? The set of "live"
// runs changes frequently and we don't want to add/remove bus
// subscriptions per WS subscriber — one wildcard subscription with a
// run-id-keyed broadcast on top of it is simpler and matches how the
// other gateway broadcasters operate.
type RunEventBroadcaster struct {
hub *Hub
subscription bus.Subscription
logger *logger.Logger
}
// RegisterRunNotifications creates a RunEventBroadcaster and wires it
// to the event bus. The broadcaster is cleaned up when ctx is cancelled.
func RegisterRunNotifications(
ctx context.Context, eventBus bus.EventBus, hub *Hub, log *logger.Logger,
) *RunEventBroadcaster {
b := &RunEventBroadcaster{
hub: hub,
logger: log.WithFields(zap.String("component", "ws-run-broadcaster")),
}
if eventBus == nil {
return b
}
sub, err := eventBus.Subscribe(events.BuildOfficeRunEventWildcardSubject(), b.handle)
if err != nil {
b.logger.Error("failed to subscribe to run-event wildcard", zap.Error(err))
return b
}
b.subscription = sub
go func() {
<-ctx.Done()
b.Close()
}()
return b
}
// Close unsubscribes the wildcard subscription.
func (b *RunEventBroadcaster) Close() {
if b.subscription == nil {
return
}
if b.subscription.IsValid() {
_ = b.subscription.Unsubscribe()
}
b.subscription = nil
}
// handle pulls run_id out of the event payload and broadcasts the
// run.event.appended action to clients subscribed to that run.
func (b *RunEventBroadcaster) handle(_ context.Context, event *bus.Event) error {
payload, ok := event.Data.(map[string]interface{})
if !ok {
return nil
}
runID, _ := payload["run_id"].(string)
if runID == "" {
return nil
}
msg, err := ws.NewNotification(ws.ActionRunEventAppended, payload)
if err != nil {
b.logger.Error("failed to build run-event notification", zap.Error(err))
return nil
}
b.hub.BroadcastToRun(runID, msg)
return nil
}