Files
kandev/backend/internal/orchestrator/streaming/hub.go
T
jcfs ba919305c2 Initial commit: Kandev with SQLite persistence and agent integration
Features:
- Unified Go binary running Task Service, Agent Manager, and Orchestrator
- SQLite persistence for boards, columns, and tasks
- Docker-based agent execution with ACP message streaming
- Augment Agent integration with session resumption support
- RESTful API for all operations (port 8080)
- In-memory event bus for service communication

Architecture:
- cmd/kandev: Single entry point for all services
- internal/task: Task, board, and column management
- internal/agent: Docker container lifecycle and ACP streaming
- internal/orchestrator: Agent coordination and monitoring
- internal/events: In-memory pub/sub event bus

Documentation:
- README.md: Project overview and quick start
- docs/ARCHITECTURE.md: System architecture (current + planned)
- docs/GETTING_STARTED.md: Development setup guide
- docs/IMPLEMENTATION_PLAN.md: Original microservices plan
- backend/NEXT_STEPS.md: Development roadmap
2026-01-10 01:30:24 +00:00

219 lines
5.1 KiB
Go

// Package streaming handles WebSocket connections for real-time ACP message streaming.
package streaming
import (
"context"
"encoding/json"
"sync"
"github.com/gorilla/websocket"
"github.com/kandev/kandev/internal/common/logger"
"github.com/kandev/kandev/pkg/acp/protocol"
"go.uber.org/zap"
)
// Client represents a WebSocket client connection
type Client struct {
ID string
conn *websocket.Conn
taskIDs map[string]bool // Tasks this client is subscribed to
send chan []byte
hub *Hub
mu sync.RWMutex
logger *logger.Logger
}
// NewClient creates a new WebSocket client
func NewClient(id string, conn *websocket.Conn, hub *Hub, log *logger.Logger) *Client {
return &Client{
ID: id,
conn: conn,
taskIDs: make(map[string]bool),
send: make(chan []byte, 256),
hub: hub,
logger: log.WithFields(zap.String("client_id", id)),
}
}
// Hub manages all WebSocket clients
type Hub struct {
// Registered clients
clients map[*Client]bool
// Clients by task ID for efficient message routing
taskClients map[string]map[*Client]bool
// Channels
register chan *Client
unregister chan *Client
broadcast chan *BroadcastMessage
mu sync.RWMutex
logger *logger.Logger
}
// BroadcastMessage contains a message to broadcast
type BroadcastMessage struct {
TaskID string
Message *protocol.Message
}
// NewHub creates a new WebSocket hub
func NewHub(log *logger.Logger) *Hub {
return &Hub{
clients: make(map[*Client]bool),
taskClients: make(map[string]map[*Client]bool),
register: make(chan *Client),
unregister: make(chan *Client),
broadcast: make(chan *BroadcastMessage, 256),
logger: log.WithFields(zap.String("component", "websocket_hub")),
}
}
// Run starts the hub processing loop
func (h *Hub) Run(ctx context.Context) {
h.logger.Info("WebSocket hub started")
defer h.logger.Info("WebSocket hub stopped")
for {
select {
case <-ctx.Done():
// Close all client connections
h.mu.Lock()
for client := range h.clients {
close(client.send)
delete(h.clients, client)
}
h.taskClients = make(map[string]map[*Client]bool)
h.mu.Unlock()
return
case client := <-h.register:
h.mu.Lock()
h.clients[client] = true
h.mu.Unlock()
h.logger.Debug("Client registered", zap.String("client_id", client.ID))
case client := <-h.unregister:
h.mu.Lock()
if _, ok := h.clients[client]; ok {
delete(h.clients, client)
close(client.send)
// Remove from all task subscriptions
for taskID := range client.taskIDs {
if clients, ok := h.taskClients[taskID]; ok {
delete(clients, client)
if len(clients) == 0 {
delete(h.taskClients, taskID)
}
}
}
}
h.mu.Unlock()
h.logger.Debug("Client unregistered", zap.String("client_id", client.ID))
case msg := <-h.broadcast:
h.mu.RLock()
clients := h.taskClients[msg.TaskID]
h.mu.RUnlock()
if len(clients) == 0 {
continue
}
data, err := json.Marshal(msg.Message)
if err != nil {
h.logger.Error("Failed to marshal message", zap.Error(err))
continue
}
for client := range clients {
select {
case client.send <- data:
default:
// Client send buffer is full, close connection
h.mu.Lock()
close(client.send)
delete(h.clients, client)
for taskID := range client.taskIDs {
if taskClients, ok := h.taskClients[taskID]; ok {
delete(taskClients, client)
if len(taskClients) == 0 {
delete(h.taskClients, taskID)
}
}
}
h.mu.Unlock()
}
}
}
}
}
// Register adds a client to the hub
func (h *Hub) Register(client *Client) {
h.register <- client
}
// Unregister removes a client from the hub
func (h *Hub) Unregister(client *Client) {
h.unregister <- client
}
// Broadcast sends a message to all clients subscribed to a task
func (h *Hub) Broadcast(taskID string, msg *protocol.Message) {
h.broadcast <- &BroadcastMessage{
TaskID: taskID,
Message: msg,
}
}
// SubscribeClient subscribes a client to a task
func (h *Hub) SubscribeClient(client *Client, taskID string) {
h.mu.Lock()
defer h.mu.Unlock()
if _, ok := h.taskClients[taskID]; !ok {
h.taskClients[taskID] = make(map[*Client]bool)
}
h.taskClients[taskID][client] = true
h.logger.Debug("Client subscribed to task",
zap.String("client_id", client.ID),
zap.String("task_id", taskID))
}
// UnsubscribeClient unsubscribes a client from a task
func (h *Hub) UnsubscribeClient(client *Client, taskID string) {
h.mu.Lock()
defer h.mu.Unlock()
if clients, ok := h.taskClients[taskID]; ok {
delete(clients, client)
if len(clients) == 0 {
delete(h.taskClients, taskID)
}
}
h.logger.Debug("Client unsubscribed from task",
zap.String("client_id", client.ID),
zap.String("task_id", taskID))
}
// GetClientCount returns the number of connected clients
func (h *Hub) GetClientCount() int {
h.mu.RLock()
defer h.mu.RUnlock()
return len(h.clients)
}
// GetTaskSubscriberCount returns the number of clients subscribed to a task
func (h *Hub) GetTaskSubscriberCount(taskID string) int {
h.mu.RLock()
defer h.mu.RUnlock()
if clients, ok := h.taskClients[taskID]; ok {
return len(clients)
}
return 0
}