- Archiver 131 .md dans docs/archive/root-md/ - Archiver 22 .json dans docs/archive/root-json/ - Conserver 7 .md utiles (README, CONTRIBUTING, CHANGELOG, etc.) - Conserver package.json, package-lock.json, turbo.json - Ajouter README d'index dans chaque archive
11 KiB
11 KiB
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:
{
"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 identifiertimestamp(string, required): ISO 8601 timestamp (RFC3339) in UTC
Optional Fields
id(string, optional): Unique message ID (UUID)data(object, optional): Message payloaderror(object, optional): Error information (for error messages)request_id(string, optional): Request ID for correlationuser_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
{
"type": "ping",
"timestamp": "2025-12-25T10:30:00Z"
}
Pong
{
"id": "550e8400-e29b-41d4-a716-446655440000",
"type": "pong",
"timestamp": "2025-12-25T10:30:00Z"
}
Error
{
"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)
{
"type": "subscribe",
"timestamp": "2025-12-25T10:30:00Z",
"data": {
"track_id": "track-uuid"
}
}
Subscribed (Server → Client)
{
"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)
{
"type": "unsubscribe",
"timestamp": "2025-12-25T10:30:00Z",
"data": {
"track_id": "track-uuid"
}
}
Unsubscribed (Server → Client)
{
"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)
{
"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)
{
"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)
{
"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)
{
"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)
{
"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)
{
"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
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
// Create an error message
errorMsg := wsmsg.NewErrorMessage(
400,
"Invalid message format",
map[string]interface{}{
"field": "type",
"reason": "Missing required field",
},
)
Parsing Messages
// 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
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
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
// 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)
{
"track_id": 123,
"type": "analytics_update",
"data": {},
"timestamp": "2025-12-25T10:30:00Z"
}
Standardized Format
{
"id": "550e8400-e29b-41d4-a716-446655440000",
"type": "analytics_update",
"timestamp": "2025-12-25T10:30:00Z",
"data": {},
"track_id": "123"
}
Key Changes
- Message ID: Added unique
idfield (UUID) - Track ID: Changed from
track_id(int64) totrack_id(string) - Timestamp: Always ISO 8601 (RFC3339) format
- Error Handling: Standardized
errorobject structure - Context Fields: Added
request_id,user_id,conversation_id
Best Practices
For Backend Developers
-
Always Use Standardized Format
msg := wsmsg.NewWebSocketMessage(msgType, data) -
Include Context Information
msg.WithTrackID(trackID). WithUserID(userID). WithRequestID(requestID) -
Use Appropriate Message Types
wsmsg.MessageTypeSubscribed wsmsg.MessageTypeAnalyticsUpdate wsmsg.MessageTypeError -
Handle Errors Properly
errorMsg := wsmsg.NewErrorMessage(400, "Invalid format", nil)
For Frontend Developers
-
Validate Messages
if (!message.type || !message.timestamp) { console.error('Invalid message'); return; } -
Handle Errors
if (message.error) { console.error('WebSocket error:', message.error); // Show user-friendly error } -
Use Type Guards
function isAnalyticsUpdate(msg: WebSocketMessage): msg is AnalyticsUpdateMessage { return msg.type === 'analytics_update'; } -
Respect Timestamps
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
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
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 specificationERROR_RESPONSE_STANDARD.md- Error format specificationveza-backend-api/internal/websocket/message.go- Backend implementationapps/web/src/types/websocket.ts- Frontend types
Last Updated: 2025-12-25
Maintained By: Veza Backend Team