7.8 KiB
7.8 KiB
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:
// Redis configuration for realtime pub/sub
RedisURL string
Add to the Load() function:
RedisURL: os.Getenv("REDIS_URL"),
Acceptance Criteria
Configstruct hasRedisURLfieldLoad()reads fromREDIS_URLenvironment 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
- Import required packages:
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"
)
- Load config and create context:
cfg := config.Load()
ctx := context.Background()
- Create hub:
hub := realtime.NewHub(logger)
go hub.Run(ctx)
- Create Redis broadcaster (optional):
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)
}
}
- Pass hub and broadcaster to route registration:
api.RegisterRoutes(application, exampleService, hub, broadcaster)
Acceptance Criteria
- Hub is created and running in a goroutine
- Redis broadcaster is created when
REDIS_URLis set - Redis broadcaster is nil when
REDIS_URLis 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
- Update function signature:
func RegisterRoutes(
application *app.App,
exampleService *service.ExampleService,
hub realtime.Hub,
broadcaster realtime.Broadcaster,
) {
- Import realtime package:
import "git.threesix.ai/jordan/sp3-verify-1770325830/pkg/realtime"
- Create and configure WebSocket handler:
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,
})
- Mount WebSocket routes:
application.Mount("/api/chat-api/ws", wsHandler.Routes())
Acceptance Criteria
- Function signature accepts
hubandbroadcasterparameters - WebSocket handler is created with appropriate callbacks
- Handler is mounted at
/api/chat-api/ws - Auth requirement respects
AUTH_ENABLEDconfig
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:
spec.WithTag("WebSocket", "Real-time WebSocket endpoints")
Add WebSocket path documentation:
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/wsendpoint 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:
- Connection upgrade - Verify WebSocket upgrade succeeds
- Message broadcast - Verify messages sent by one client reach others
- Room isolation - Verify room messages don't leak to other rooms
- Ping/pong - Verify heartbeat mechanism
Example test structure:
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