# Implementation Tasks: WebSocket Chat ## Task Overview | ID | Task | Status | Blocked By | |----|------|--------|------------| | WS-1 | Extend config with Redis URL | pending | - | | WS-2 | Wire realtime components in main.go | pending | WS-1 | | WS-3 | Mount WebSocket handler in routes.go | pending | WS-2 | | WS-4 | Add WebSocket documentation to spec.go | pending | WS-3 | | WS-5 | Add integration tests for WebSocket chat | pending | WS-3 | --- ## WS-1: Extend config with Redis URL **Status:** pending ### Description Add Redis URL configuration to the chat-api service config. ### Files to Modify - `services/chat-api/internal/config/config.go` ### Implementation Details Add to the `Config` struct: ```go // Redis configuration for realtime pub/sub RedisURL string ``` Add to the `Load()` function: ```go RedisURL: os.Getenv("REDIS_URL"), ``` ### Acceptance Criteria - [ ] `Config` struct has `RedisURL` field - [ ] `Load()` reads from `REDIS_URL` environment variable - [ ] Empty string is valid (disables Redis, uses local-only broadcast) --- ## WS-2: Wire realtime components in main.go **Status:** pending **Blocked By:** WS-1 ### Description Initialize the realtime hub and Redis broadcaster in the service entry point. ### Files to Modify - `services/chat-api/cmd/server/main.go` ### Implementation Details 1. Import required packages: ```go import ( "context" "github.com/redis/go-redis/v9" "git.threesix.ai/jordan/sp3-verify-1770325830/pkg/realtime" "git.threesix.ai/jordan/sp3-verify-1770325830/services/chat-api/internal/config" ) ``` 2. Load config and create context: ```go cfg := config.Load() ctx := context.Background() ``` 3. Create hub: ```go hub := realtime.NewHub(logger) go hub.Run(ctx) ``` 4. Create Redis broadcaster (optional): ```go var broadcaster realtime.Broadcaster if cfg.RedisURL != "" { opts, err := redis.ParseURL(cfg.RedisURL) if err != nil { logger.Error("invalid redis url", "error", err) } else { redisClient := redis.NewClient(opts) broadcaster = realtime.NewRedisBroadcaster(redisClient, hub, logger) go broadcaster.Run(ctx) } } ``` 5. Pass hub and broadcaster to route registration: ```go api.RegisterRoutes(application, exampleService, hub, broadcaster) ``` ### Acceptance Criteria - [ ] Hub is created and running in a goroutine - [ ] Redis broadcaster is created when `REDIS_URL` is set - [ ] Redis broadcaster is nil when `REDIS_URL` is empty - [ ] Hub and broadcaster are passed to route registration --- ## WS-3: Mount WebSocket handler in routes.go **Status:** pending **Blocked By:** WS-2 ### Description Update the route registration to accept realtime components and mount the WebSocket handler. ### Files to Modify - `services/chat-api/internal/api/routes.go` ### Implementation Details 1. Update function signature: ```go func RegisterRoutes( application *app.App, exampleService *service.ExampleService, hub realtime.Hub, broadcaster realtime.Broadcaster, ) { ``` 2. Import realtime package: ```go import "git.threesix.ai/jordan/sp3-verify-1770325830/pkg/realtime" ``` 3. Create and configure WebSocket handler: ```go wsHandler := realtime.NewHandler(hub, logger, realtime.HandlerConfig{ Broadcaster: broadcaster, OnConnect: func(conn realtime.Connection) { logger.Info("websocket client connected", "client_id", conn.ID(), "user_id", conn.UserID(), ) }, OnDisconnect: func(conn realtime.Connection) { logger.Info("websocket client disconnected", "client_id", conn.ID(), ) }, OnMessage: func(conn realtime.Connection, msg *realtime.Message) *realtime.Message { if msg.Type == "" { msg.Type = realtime.MessageTypeChat } return msg }, AuthRequired: cfg.AuthEnabled, }) ``` 4. Mount WebSocket routes: ```go application.Mount("/api/chat-api/ws", wsHandler.Routes()) ``` ### Acceptance Criteria - [ ] Function signature accepts `hub` and `broadcaster` parameters - [ ] WebSocket handler is created with appropriate callbacks - [ ] Handler is mounted at `/api/chat-api/ws` - [ ] Auth requirement respects `AUTH_ENABLED` config --- ## WS-4: Add WebSocket documentation to spec.go **Status:** pending **Blocked By:** WS-3 ### Description Document the WebSocket endpoint in the OpenAPI specification. ### Files to Modify - `services/chat-api/internal/api/spec.go` ### Implementation Details Add WebSocket tag: ```go spec.WithTag("WebSocket", "Real-time WebSocket endpoints") ``` Add WebSocket path documentation: ```go spec.AddPath("/api/chat-api/ws", "get", map[string]any{ "summary": "WebSocket connection", "description": "Upgrades to WebSocket for real-time chat. Messages are broadcast to all connected clients via Redis pub/sub.", "tags": []string{"WebSocket"}, "responses": map[string]any{ "101": map[string]any{ "description": "Switching Protocols - WebSocket upgrade successful", }, "401": openapi.OpResponse("Unauthorized - authentication required", nil), }, }) spec.AddPath("/api/chat-api/ws/{room}", "get", map[string]any{ "summary": "WebSocket connection to room", "description": "Upgrades to WebSocket and joins the specified room. Messages are broadcast only to clients in the same room.", "tags": []string{"WebSocket"}, "parameters": []map[string]any{ { "name": "room", "in": "path", "required": true, "description": "Room identifier to join", "schema": map[string]any{"type": "string"}, }, }, "responses": map[string]any{ "101": map[string]any{ "description": "Switching Protocols - WebSocket upgrade successful", }, "401": openapi.OpResponse("Unauthorized - authentication required", nil), }, }) ``` ### Acceptance Criteria - [ ] WebSocket tag added to spec - [ ] `/api/chat-api/ws` endpoint documented - [ ] `/api/chat-api/ws/{room}` endpoint documented with room parameter - [ ] Response codes documented (101, 401) --- ## WS-5: Add integration tests for WebSocket chat **Status:** pending **Blocked By:** WS-3 ### Description Add tests to verify WebSocket functionality. ### Files to Create - `services/chat-api/internal/api/handlers/websocket_test.go` ### Implementation Details Test cases: 1. **Connection upgrade** - Verify WebSocket upgrade succeeds 2. **Message broadcast** - Verify messages sent by one client reach others 3. **Room isolation** - Verify room messages don't leak to other rooms 4. **Ping/pong** - Verify heartbeat mechanism Example test structure: ```go package handlers_test import ( "net/http" "net/http/httptest" "testing" "time" "github.com/gorilla/websocket" "git.threesix.ai/jordan/sp3-verify-1770325830/pkg/logging" "git.threesix.ai/jordan/sp3-verify-1770325830/pkg/realtime" ) func TestWebSocket_Connection(t *testing.T) { // Create hub hub := realtime.NewHub(logging.Nop()) ctx, cancel := context.WithCancel(context.Background()) defer cancel() go hub.Run(ctx) // Create handler handler := realtime.NewHandler(hub, logging.Nop(), realtime.HandlerConfig{}) // Create test server server := httptest.NewServer(handler.Routes()) defer server.Close() // Connect WebSocket wsURL := "ws" + server.URL[4:] + "/" conn, _, err := websocket.DefaultDialer.Dial(wsURL, nil) if err != nil { t.Fatalf("dial failed: %v", err) } defer conn.Close() // Verify connection count time.Sleep(50 * time.Millisecond) if hub.ConnectionCount() != 1 { t.Errorf("expected 1 connection, got %d", hub.ConnectionCount()) } } ``` ### Acceptance Criteria - [ ] Test WebSocket connection upgrade works - [ ] Test message broadcast to multiple clients - [ ] Test room-based message isolation - [ ] All tests pass