sp3-verify-1770325830/.sdlc/features/websocket-chat/design.md
rdev-worker 42c1444274
All checks were successful
ci/woodpecker/push/woodpecker Pipeline was successful
build: /implement-feature websocket-chat --requirements 'GET /ws upgrades to...
2026-02-05 21:50:17 +00:00

211 lines
8.0 KiB
Markdown

# Technical Design: WebSocket Chat
## Architecture Overview
This feature integrates the existing `pkg/realtime` package into the `chat-api` service to provide WebSocket chat functionality with Redis pub/sub for horizontal scaling.
```
┌─────────────┐ ┌─────────────┐ ┌─────────────┐
│ Client 1 │ │ Client 2 │ │ Client N │
└──────┬──────┘ └──────┬──────┘ └──────┬──────┘
│ WebSocket │ WebSocket │ WebSocket
▼ ▼ ▼
┌──────────────────────────────────────────────────────┐
│ chat-api Pod 1 │
│ ┌─────────────────────────────────────────────────┐ │
│ │ realtime.Handler │ │
│ │ ┌─────────────────────────────────────────┐ │ │
│ │ │ LocalHub │ │ │
│ │ │ - connections map[id]Connection │ │ │
│ │ │ - rooms map[room]map[id]struct{} │ │ │
│ │ └─────────────────────────────────────────┘ │ │
│ └─────────────────────────────────────────────────┘ │
│ │ │
│ ┌─────────────────────────────────────────────────┐ │
│ │ RedisBroadcaster │ │
│ │ - Publish() → Redis channel │ │
│ │ - Run() ← Redis PSubscribe │ │
│ └─────────────────────────────────────────────────┘ │
└──────────────────────────┬───────────────────────────┘
┌────────────┴────────────┐
│ Redis Pub/Sub │
│ realtime:global │
│ realtime:room:{name} │
└────────────┬────────────┘
┌──────────────────────────┴───────────────────────────┐
│ chat-api Pod 2 │
│ (same structure) │
└──────────────────────────────────────────────────────┘
```
## Component Design
### 1. Configuration Extension
Extend `internal/config/config.go` to include Redis configuration:
```go
type Config struct {
// ... existing fields ...
// Redis
RedisURL string
}
func Load() *Config {
return &Config{
// ... existing ...
RedisURL: getEnv("REDIS_URL", "redis://localhost:6379"),
}
}
```
### 2. Service Bootstrap (main.go)
Wire up the realtime components in the service entry point:
```go
func main() {
cfg := config.Load()
ctx := context.Background()
// Create hub (local connection registry)
hub := realtime.NewHub(logger)
go hub.Run(ctx)
// Create Redis broadcaster (for multi-pod)
var broadcaster realtime.Broadcaster
if cfg.RedisURL != "" {
redisClient := redis.NewClient(parseRedisURL(cfg.RedisURL))
broadcaster = realtime.NewRedisBroadcaster(redisClient, hub, logger)
go broadcaster.Run(ctx)
}
// Register routes with realtime handler
api.RegisterRoutes(application, exampleService, hub, broadcaster)
}
```
### 3. Route Registration
Add WebSocket routes in `internal/api/routes.go`:
```go
func RegisterRoutes(
application *app.App,
exampleService *service.ExampleService,
hub realtime.Hub,
broadcaster realtime.Broadcaster,
) {
// ... existing routes ...
// WebSocket handler
wsHandler := realtime.NewHandler(hub, logger, realtime.HandlerConfig{
Broadcaster: broadcaster,
OnConnect: func(conn realtime.Connection) {
logger.Info("client connected", "id", conn.ID())
},
OnDisconnect: func(conn realtime.Connection) {
logger.Info("client disconnected", "id", conn.ID())
},
OnMessage: func(conn realtime.Connection, msg *realtime.Message) *realtime.Message {
// Set message type if not specified
if msg.Type == "" {
msg.Type = realtime.MessageTypeChat
}
return msg
},
AuthRequired: cfg.AuthEnabled,
})
// Mount WebSocket routes
application.Mount("/api/chat-api/ws", wsHandler.Routes())
}
```
### 4. Message Flow
1. **Client → Server (Incoming)**
- Client sends JSON message via WebSocket
- `WSClient.readPump()` decodes message
- `OnMessage` callback transforms/filters
- `RedisBroadcaster.Publish()` sends to Redis channel
2. **Redis → All Pods (Distribution)**
- Redis delivers to all subscribed pods
- `RedisBroadcaster.Run()` receives via PSubscribe
- Filters out messages from same pod (echo prevention)
- Forwards to `LocalHub.Broadcast()`
3. **Server → Clients (Outgoing)**
- `LocalHub.doBroadcast()` iterates room connections
- `WSClient.Send()` queues message
- `WSClient.writePump()` writes to WebSocket
### 5. Redis Channel Naming
| Pattern | Usage |
|---------|-------|
| `realtime:global` | Messages without room (broadcast to all) |
| `realtime:room:{name}` | Room-specific messages |
### 6. Graceful Shutdown
The existing `pkg/realtime` package handles:
- Context cancellation triggers hub shutdown
- Hub closes all connections
- WSClient sends close frame
- Redis broadcaster closes subscription
## File Changes
| File | Change |
|------|--------|
| `services/chat-api/internal/config/config.go` | Add `RedisURL` field |
| `services/chat-api/cmd/server/main.go` | Wire hub, broadcaster, Redis client |
| `services/chat-api/internal/api/routes.go` | Mount WebSocket handler |
| `services/chat-api/internal/api/spec.go` | Add WebSocket endpoint documentation |
## Testing Strategy
### Unit Tests
- Config loading with Redis URL
- Message transformation in OnMessage callback
### Integration Tests
- WebSocket connection upgrade
- Message send/receive flow
- Room-based isolation
- Multi-client broadcast
### Manual Testing
```bash
# Terminal 1: Start server
./scripts/dev.sh
# Terminal 2: Connect with wscat
wscat -c ws://localhost:8001/api/chat-api/ws
# Terminal 3: Another client
wscat -c ws://localhost:8001/api/chat-api/ws
# Send message from Terminal 2, verify received in Terminal 3
{"type":"chat","data":{"content":"Hello!"}}
```
## Security Considerations
1. **Authentication**: Optional auth via `AUTH_ENABLED` config
2. **Origin Check**: WebSocket upgrader validates Origin header (configurable)
3. **Message Size**: 64KB limit prevents memory exhaustion
4. **Buffer Limits**: 256-message buffer drops excess (prevents slow-client backpressure)
## Rollout Plan
1. Deploy with `REDIS_URL` set to enable cross-pod messaging
2. Without `REDIS_URL`, operates in single-pod mode (local broadcast only)
3. No database migrations required
4. Feature is additive (no breaking changes)