127 lines
3.7 KiB
Go
127 lines
3.7 KiB
Go
package websocket
|
|
|
|
import (
|
|
"context"
|
|
"encoding/json"
|
|
"sync"
|
|
|
|
"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"
|
|
)
|
|
|
|
// CanvasEventBroadcaster forwards the content-free canvas lifecycle events to
|
|
// the owner of the canvas workspace. Application data belongs on the
|
|
// capability-bound SSE protocol, not on this shared WebSocket channel.
|
|
type CanvasEventBroadcaster struct {
|
|
hub *Hub
|
|
subscriptions []bus.Subscription
|
|
logger *logger.Logger
|
|
closeMu sync.Mutex
|
|
closed bool
|
|
}
|
|
|
|
// RegisterCanvasNotifications wires the gated canvas lifecycle events to the
|
|
// existing workspace-scoped WebSocket hub. Callers must only invoke it when
|
|
// features.canvases is enabled.
|
|
func RegisterCanvasNotifications(ctx context.Context, eventBus bus.EventBus, hub *Hub, log *logger.Logger) *CanvasEventBroadcaster {
|
|
b := &CanvasEventBroadcaster{
|
|
hub: hub,
|
|
logger: log.WithFields(zap.String("component", "ws-canvas-broadcaster")),
|
|
}
|
|
if eventBus == nil || hub == nil {
|
|
return b
|
|
}
|
|
|
|
for _, event := range []struct {
|
|
subject string
|
|
action string
|
|
}{
|
|
{events.CanvasCreated, ws.ActionCanvasCreated},
|
|
{events.CanvasUpdated, ws.ActionCanvasUpdated},
|
|
{events.CanvasReleaseActivated, ws.ActionCanvasReleaseActivated},
|
|
{events.CanvasReleasePermissionRequired, ws.ActionCanvasReleasePermissionRequired},
|
|
{events.CanvasPromoted, ws.ActionCanvasPromoted},
|
|
{events.CanvasWorkspaceDataEnabled, ws.ActionCanvasWorkspaceDataEnabled},
|
|
{events.CanvasArchived, ws.ActionCanvasArchived},
|
|
{events.CanvasRestored, ws.ActionCanvasRestored},
|
|
{events.CanvasRemoved, ws.ActionCanvasRemoved},
|
|
} {
|
|
b.subscribe(eventBus, event.subject, event.action)
|
|
}
|
|
|
|
go func() {
|
|
<-ctx.Done()
|
|
b.Close()
|
|
}()
|
|
return b
|
|
}
|
|
|
|
// Close releases event-bus subscriptions. It is safe to call more than once.
|
|
func (b *CanvasEventBroadcaster) Close() {
|
|
b.closeMu.Lock()
|
|
if b.closed {
|
|
b.closeMu.Unlock()
|
|
return
|
|
}
|
|
b.closed = true
|
|
subscriptions := b.subscriptions
|
|
b.subscriptions = nil
|
|
b.closeMu.Unlock()
|
|
|
|
for _, sub := range subscriptions {
|
|
if sub != nil && sub.IsValid() {
|
|
_ = sub.Unsubscribe()
|
|
}
|
|
}
|
|
}
|
|
|
|
func (b *CanvasEventBroadcaster) subscribe(eventBus bus.EventBus, subject, action string) {
|
|
sub, err := eventBus.Subscribe(subject, func(ctx context.Context, event *bus.Event) error {
|
|
msg, err := ws.NewNotification(action, event.Data)
|
|
if err != nil {
|
|
b.logger.Error("failed to build canvas websocket notification", zap.String("action", action), zap.Error(err))
|
|
return nil
|
|
}
|
|
workspaceID := canvasWorkspaceID(event.Data)
|
|
// Canvas events are always workspace-scoped. Dropping an unattributed
|
|
// event under auth is safer than allowing it to fan out globally.
|
|
b.hub.BroadcastToWorkspaceOrDrop(workspaceID, msg)
|
|
return nil
|
|
})
|
|
if err != nil {
|
|
b.logger.Error("failed to subscribe to canvas events", zap.String("subject", subject), zap.Error(err))
|
|
return
|
|
}
|
|
b.closeMu.Lock()
|
|
if b.closed {
|
|
b.closeMu.Unlock()
|
|
if sub.IsValid() {
|
|
_ = sub.Unsubscribe()
|
|
}
|
|
return
|
|
}
|
|
b.subscriptions = append(b.subscriptions, sub)
|
|
b.closeMu.Unlock()
|
|
}
|
|
|
|
func canvasWorkspaceID(data interface{}) string {
|
|
if workspaceID := extractWorkspaceID(data); workspaceID != "" {
|
|
return workspaceID
|
|
}
|
|
// Memory-event callers often pass a typed LifecycleEvent while NATS
|
|
// callers pass a JSON-shaped map. Normalize the former without making the
|
|
// gateway depend on the canvas package's concrete event type.
|
|
encoded, err := json.Marshal(data)
|
|
if err != nil {
|
|
return ""
|
|
}
|
|
var fields map[string]interface{}
|
|
if err := json.Unmarshal(encoded, &fields); err != nil {
|
|
return ""
|
|
}
|
|
return extractWorkspaceID(fields)
|
|
}
|