veza/docs/archive/root-md/WEBSOCKET_MESSAGE_FORMAT.md

544 lines
11 KiB
Markdown
Raw Normal View History

# WebSocket Message Format Standardization
## INT-014: Add WebSocket message format standardization
**Date**: 2025-12-25
**Status**: Completed
## Overview
This document defines the standardized format for all WebSocket messages in the Veza platform. It ensures consistency between backend and frontend, making message handling predictable and maintainable.
## Standard Message Format
All WebSocket messages follow this standardized structure:
```json
{
"id": "550e8400-e29b-41d4-a716-446655440000",
"type": "message_type",
"timestamp": "2025-12-25T10:30:00Z",
"data": {},
"error": null,
"request_id": "req-123",
"user_id": "user-uuid",
"track_id": "track-uuid",
"conversation_id": "conv-uuid"
}
```
### Required Fields
- **`type`** (string, required): Message type identifier
- **`timestamp`** (string, required): ISO 8601 timestamp (RFC3339) in UTC
### Optional Fields
- **`id`** (string, optional): Unique message ID (UUID)
- **`data`** (object, optional): Message payload
- **`error`** (object, optional): Error information (for error messages)
- **`request_id`** (string, optional): Request ID for correlation
- **`user_id`** (string, optional): User ID (UUID)
- **`track_id`** (string, optional): Track ID (UUID or string)
- **`conversation_id`** (string, optional): Conversation ID (UUID)
## Message Types
### Connection Messages
#### Ping
```json
{
"type": "ping",
"timestamp": "2025-12-25T10:30:00Z"
}
```
#### Pong
```json
{
"id": "550e8400-e29b-41d4-a716-446655440000",
"type": "pong",
"timestamp": "2025-12-25T10:30:00Z"
}
```
#### Error
```json
{
"id": "550e8400-e29b-41d4-a716-446655440000",
"type": "error",
"timestamp": "2025-12-25T10:30:00Z",
"error": {
"code": 400,
"message": "Invalid message format",
"details": {
"field": "type",
"reason": "Missing required field"
}
}
}
```
### Subscription Messages
#### Subscribe (Client → Server)
```json
{
"type": "subscribe",
"timestamp": "2025-12-25T10:30:00Z",
"data": {
"track_id": "track-uuid"
}
}
```
#### Subscribed (Server → Client)
```json
{
"id": "550e8400-e29b-41d4-a716-446655440000",
"type": "subscribed",
"timestamp": "2025-12-25T10:30:00Z",
"data": {
"track_id": "track-uuid"
},
"track_id": "track-uuid"
}
```
#### Unsubscribe (Client → Server)
```json
{
"type": "unsubscribe",
"timestamp": "2025-12-25T10:30:00Z",
"data": {
"track_id": "track-uuid"
}
}
```
#### Unsubscribed (Server → Client)
```json
{
"id": "550e8400-e29b-41d4-a716-446655440000",
"type": "unsubscribed",
"timestamp": "2025-12-25T10:30:00Z",
"data": {
"track_id": "track-uuid"
},
"track_id": "track-uuid"
}
```
### Chat Messages
#### Chat Message (Server → Client)
```json
{
"id": "550e8400-e29b-41d4-a716-446655440000",
"type": "chat_message",
"timestamp": "2025-12-25T10:30:00Z",
"data": {
"message": {
"id": "msg-uuid",
"conversation_id": "conv-uuid",
"sender_id": "user-uuid",
"content": "Hello, world!",
"created_at": "2025-12-25T10:30:00Z"
}
},
"conversation_id": "conv-uuid"
}
```
#### Typing Indicator (Server → Client)
```json
{
"id": "550e8400-e29b-41d4-a716-446655440000",
"type": "typing",
"timestamp": "2025-12-25T10:30:00Z",
"data": {
"user_id": "user-uuid",
"conversation_id": "conv-uuid",
"is_typing": true
},
"conversation_id": "conv-uuid"
}
```
### Playback Messages
#### Analytics Update (Server → Client)
```json
{
"id": "550e8400-e29b-41d4-a716-446655440000",
"type": "analytics_update",
"timestamp": "2025-12-25T10:30:00Z",
"data": {
"id": "analytics-uuid",
"track_id": "track-uuid",
"user_id": "user-uuid",
"play_time": 120,
"pause_count": 2,
"seek_count": 1,
"completion_rate": 0.75
},
"track_id": "track-uuid"
}
```
#### Stats Update (Server → Client)
```json
{
"id": "550e8400-e29b-41d4-a716-446655440000",
"type": "stats_update",
"timestamp": "2025-12-25T10:30:00Z",
"data": {
"total_sessions": 100,
"total_play_time": 3600,
"average_play_time": 36,
"completion_rate": 0.65
},
"track_id": "track-uuid"
}
```
#### Playback State (Server → Client)
```json
{
"id": "550e8400-e29b-41d4-a716-446655440000",
"type": "playback_state",
"timestamp": "2025-12-25T10:30:00Z",
"data": {
"track_id": "track-uuid",
"user_id": "user-uuid",
"position": 45.5,
"is_playing": true,
"volume": 0.8
},
"track_id": "track-uuid"
}
```
### Notification Messages
#### Notification (Server → Client)
```json
{
"id": "550e8400-e29b-41d4-a716-446655440000",
"type": "notification",
"timestamp": "2025-12-25T10:30:00Z",
"data": {
"id": "notif-uuid",
"user_id": "user-uuid",
"type": "new_message",
"content": "You have a new message",
"link": "/conversations/conv-uuid",
"read": false,
"created_at": "2025-12-25T10:30:00Z"
},
"user_id": "user-uuid"
}
```
## Backend Implementation
### Creating Messages
```go
import wsmsg "veza-backend-api/internal/websocket"
// Create a simple message
msg := wsmsg.NewWebSocketMessage(
wsmsg.MessageTypeSubscribed,
gin.H{"track_id": trackID},
)
// Add context information
msg.WithTrackID(trackID).
WithUserID(userID.String()).
WithRequestID(requestID)
// Convert to JSON
data, err := msg.ToJSON()
```
### Error Messages
```go
// Create an error message
errorMsg := wsmsg.NewErrorMessage(
400,
"Invalid message format",
map[string]interface{}{
"field": "type",
"reason": "Missing required field",
},
)
```
### Parsing Messages
```go
// Parse incoming message
msg, err := wsmsg.ParseWebSocketMessage(messageBytes)
if err != nil {
// Handle error
}
// Validate message
if !msg.IsValid() {
// Handle invalid message
}
// Access message fields
switch msg.Type {
case "subscribe":
// Handle subscription
case "ping":
// Handle ping
}
```
## Frontend Implementation
### TypeScript Types
```typescript
interface WebSocketMessage {
id?: string;
type: string;
timestamp: string;
data?: unknown;
error?: {
code: number;
message: string;
details?: Record<string, unknown>;
};
request_id?: string;
user_id?: string;
track_id?: string;
conversation_id?: string;
}
```
### Handling Messages
```typescript
ws.onmessage = (event) => {
try {
const message: WebSocketMessage = JSON.parse(event.data);
// Validate message
if (!message.type || !message.timestamp) {
console.error('Invalid WebSocket message format');
return;
}
// Handle by type
switch (message.type) {
case 'subscribed':
handleSubscribed(message);
break;
case 'analytics_update':
handleAnalyticsUpdate(message);
break;
case 'error':
handleError(message);
break;
default:
console.warn('Unknown message type:', message.type);
}
} catch (error) {
console.error('Failed to parse WebSocket message:', error);
}
};
```
### Sending Messages
```typescript
// Send a subscribe message
const subscribeMessage: WebSocketMessage = {
type: 'subscribe',
timestamp: new Date().toISOString(),
data: {
track_id: trackId,
},
};
ws.send(JSON.stringify(subscribeMessage));
```
## Migration from Legacy Format
### Legacy Format (Deprecated)
```json
{
"track_id": 123,
"type": "analytics_update",
"data": {},
"timestamp": "2025-12-25T10:30:00Z"
}
```
### Standardized Format
```json
{
"id": "550e8400-e29b-41d4-a716-446655440000",
"type": "analytics_update",
"timestamp": "2025-12-25T10:30:00Z",
"data": {},
"track_id": "123"
}
```
### Key Changes
1. **Message ID**: Added unique `id` field (UUID)
2. **Track ID**: Changed from `track_id` (int64) to `track_id` (string)
3. **Timestamp**: Always ISO 8601 (RFC3339) format
4. **Error Handling**: Standardized `error` object structure
5. **Context Fields**: Added `request_id`, `user_id`, `conversation_id`
## Best Practices
### For Backend Developers
1. **Always Use Standardized Format**
```go
msg := wsmsg.NewWebSocketMessage(msgType, data)
```
2. **Include Context Information**
```go
msg.WithTrackID(trackID).
WithUserID(userID).
WithRequestID(requestID)
```
3. **Use Appropriate Message Types**
```go
wsmsg.MessageTypeSubscribed
wsmsg.MessageTypeAnalyticsUpdate
wsmsg.MessageTypeError
```
4. **Handle Errors Properly**
```go
errorMsg := wsmsg.NewErrorMessage(400, "Invalid format", nil)
```
### For Frontend Developers
1. **Validate Messages**
```typescript
if (!message.type || !message.timestamp) {
console.error('Invalid message');
return;
}
```
2. **Handle Errors**
```typescript
if (message.error) {
console.error('WebSocket error:', message.error);
// Show user-friendly error
}
```
3. **Use Type Guards**
```typescript
function isAnalyticsUpdate(msg: WebSocketMessage): msg is AnalyticsUpdateMessage {
return msg.type === 'analytics_update';
}
```
4. **Respect Timestamps**
```typescript
const messageTime = new Date(message.timestamp);
const now = new Date();
const delay = now.getTime() - messageTime.getTime();
```
## Message Type Reference
| Type | Direction | Description |
|------|-----------|-------------|
| `ping` | Client → Server | Keep-alive ping |
| `pong` | Server → Client | Keep-alive pong |
| `error` | Server → Client | Error message |
| `subscribe` | Client → Server | Subscribe to updates |
| `unsubscribe` | Client → Server | Unsubscribe from updates |
| `subscribed` | Server → Client | Subscription confirmed |
| `unsubscribed` | Server → Client | Unsubscription confirmed |
| `chat_message` | Server → Client | New chat message |
| `typing` | Bidirectional | Typing indicator |
| `read_receipt` | Bidirectional | Read receipt |
| `analytics_update` | Server → Client | Playback analytics update |
| `stats_update` | Server → Client | Playback stats update |
| `playback_state` | Bidirectional | Playback state sync |
| `notification` | Server → Client | Notification event |
## Error Codes
| Code | Description |
|------|-------------|
| 400 | Bad Request - Invalid message format |
| 401 | Unauthorized - Authentication required |
| 403 | Forbidden - Insufficient permissions |
| 404 | Not Found - Resource not found |
| 429 | Too Many Requests - Rate limit exceeded |
| 500 | Internal Server Error - Server error |
## Testing
### Backend Tests
```go
func TestWebSocketMessage(t *testing.T) {
msg := wsmsg.NewWebSocketMessage(
wsmsg.MessageTypeSubscribed,
gin.H{"track_id": "123"},
)
assert.NotEmpty(t, msg.ID)
assert.Equal(t, "subscribed", msg.Type)
assert.NotEmpty(t, msg.Timestamp)
assert.True(t, msg.IsValid())
}
```
### Frontend Tests
```typescript
describe('WebSocket Message', () => {
it('should parse valid message', () => {
const message = {
id: '123',
type: 'subscribed',
timestamp: '2025-12-25T10:30:00Z',
data: { track_id: 'track-123' },
};
expect(message.type).toBe('subscribed');
expect(message.timestamp).toBeDefined();
});
});
```
## References
- `DATETIME_STANDARD.md` - Date/time format specification
- `ERROR_RESPONSE_STANDARD.md` - Error format specification
- `veza-backend-api/internal/websocket/message.go` - Backend implementation
- `apps/web/src/types/websocket.ts` - Frontend types
---
**Last Updated**: 2025-12-25
**Maintained By**: Veza Backend Team