This document specifies the WebSocket implementation for real-time dashboard communications, including connection management, event specifications, and reconnection logic.
WebSocket: ws://host:port/api/v1/dashboard
All WebSocket connections require JWT authentication. Include the token in one of the following ways:
-
Authorization Header (preferred):
Authorization: Bearer <jwt_token> -
Auth Object:
io(url, { auth: { token: "<jwt_token>" } });
-
Query Parameter:
ws://host:port/api/v1/dashboard?token=<jwt_token>
Client → Server:
// Connect with authentication
const socket = io("http://localhost:3001/api/v1/dashboard", {
auth: { token: "your_jwt_token" },
transports: ["websocket"]
});Server → Client:
{
"event": "connection:established",
"data": {
"clientId": "abc123",
"timestamp": "2024-01-15T10:30:00.000Z",
"heartbeatInterval": 30000
}
}When a client reconnects after disconnection, the server automatically replays buffered events:
Server → Client:
{
"event": "reconnection:success",
"data": {
"missedEvents": 5,
"events": [
{
"event": "portfolio:update",
"data": { "portfolioId": "xyz", "totalValue": 10000 },
"timestamp": "2024-01-15T10:25:00.000Z"
}
]
}
}| Event | Direction | Description |
|---|---|---|
connection:established |
Server → Client | Confirms successful connection |
connection:stale |
Server → Client | Notifies client connection is stale |
reconnection:success |
Server → Client | Confirms reconnection with buffered events |
| Event | Direction | Description |
|---|---|---|
heartbeat |
Server → Client | Periodic heartbeat (every 30 seconds) |
ping |
Client → Server | Client heartbeat request |
pong |
Server → Client | Server heartbeat response |
| Event | Direction | Payload | Description |
|---|---|---|---|
portfolio:subscribe |
Client → Server | { portfolioId: string } |
Subscribe to portfolio updates |
portfolio:subscription:confirmed |
Server → Client | { portfolioId, subscribedAt } |
Subscription confirmed |
portfolio:unsubscribe |
Client → Server | { portfolioId: string } |
Unsubscribe from portfolio |
portfolio:unsubscription:confirmed |
Server → Client | { portfolioId } |
Unsubscription confirmed |
| Event | Direction | Payload | Description |
|---|---|---|---|
portfolio:update |
Server → Client | PortfolioUpdatePayload | Portfolio value update |
portfolio:performance:update |
Server → Client | PerformanceUpdatePayload | Performance metrics update |
portfolio:allocation:update |
Server → Client | AllocationUpdatePayload | Asset allocation update |
portfolio:risk:update |
Server → Client | RiskUpdatePayload | Risk assessment update |
portfolio:holdings:update |
Server → Client | HoldingsPayload | Holdings update |
| Event | Direction | Payload | Description |
|---|---|---|---|
events:replay |
Client → Server | { since: string } |
Request events since timestamp |
events:replayed |
Server → Client | { count, events } |
Replayed events response |
| Event | Direction | Payload | Description |
|---|---|---|---|
error |
Server → Client | { code, message, details? } |
Error notification |
interface PortfolioUpdatePayload {
portfolioId: string;
totalValue: number;
change: number;
changePercent: number;
timestamp: string;
}interface PerformanceUpdatePayload {
portfolioId: string;
performance: {
value: number;
change: number;
changePercent: number;
timeRange: string;
};
history: Array<{
timestamp: string;
value: number;
}>;
}interface AllocationUpdatePayload {
portfolioId: string;
allocation: Array<{
asset: string;
percentage: number;
value: number;
}>;
lastUpdated: string;
}interface RiskUpdatePayload {
portfolioId: string;
riskScore: number;
riskLevel: "low" | "medium" | "high";
factors: string[];
lastUpdated: string;
}- Interval: 30 seconds
- Timeout: 5 seconds for pong response
- Stale Threshold: 5 minutes (connection closed if no heartbeat response)
The client implements exponential backoff with the following parameters:
| Parameter | Default | Max | Description |
|---|---|---|---|
| baseDelay | 1000ms | - | Starting delay |
| maxDelay | 30000ms | 30000ms | Maximum delay |
| factor | 2 | - | Multiplier for each attempt |
| jitter | 30% | - | Random variation |
Reconnection Sequence:
- First attempt: 1s
- Second attempt: 2s
- Third attempt: 4s
- ... (doubling each time)
- Max: 30s (capped)
- Max Events Per User: 1000
- Buffer Duration: 5 minutes
- Replay on Reconnect: Automatic
| Parameter | Default | Description |
|---|---|---|
| maxConnections | 100 | Max connections per upstream service |
| connectionTimeout | 10s | Connection establishment timeout |
| heartbeatInterval | 30s | Heartbeat interval |
| staleThreshold | 5min | Time before connection considered stale |
| reconnectDelay | 1s | Base reconnection delay |
| maxReconnectDelay | 30s | Maximum reconnection delay |
The following Prometheus metrics are exposed:
trellis_ws_connections_total- Total WebSocket connections by namespace and typetrellis_ws_active_connections- Active WebSocket connections by namespace
trellis_ws_subscriptions_total- Total subscriptions by namespace and channeltrellis_ws_active_subscriptions- Active subscriptions by namespace and channel
trellis_ws_heartbeats_sent_total- Heartbeats sent by namespacetrellis_ws_heartbeats_received_total- Heartbeat responses receivedtrellis_ws_heartbeat_latency_seconds- Heartbeat response latency
trellis_ws_events_buffered- Events in buffer by namespace and usertrellis_ws_events_sent_total- Events sent by namespace and event typetrellis_ws_events_received_total- Events received by namespace and event type
trellis_ws_reconnections_total- Reconnection attempts by namespace and statustrellis_ws_reconnection_delay_seconds- Reconnection delay histogram
trellis_upstream_pool_connections_total- Total pool connectionstrellis_upstream_pool_active_connections- Active pool connectionstrellis_upstream_pool_utilization_percent- Pool utilization percentagetrellis_upstream_pool_requests_total- Pool requests by status
import { io } from "socket.io-client";
const socket = io("http://localhost:3001/api/v1/dashboard", {
auth: { token: "your_jwt_token" },
transports: ["websocket"]
});
socket.on("connection:established", (data) => {
console.log("Connected:", data);
// Subscribe to portfolio updates
socket.emit("portfolio:subscribe", { portfolioId: "port_123" });
});
socket.on("portfolio:update", (data) => {
console.log("Portfolio update:", data);
});
// Handle disconnection with auto-reconnect
socket.on("disconnect", (reason) => {
console.log("Disconnected:", reason);
// Reconnection is automatic
});// In your service
@Inject()
private readonly dashboardGateway: DashboardGateway;
// Broadcast to all subscribers of a portfolio
this.dashboardGateway.broadcastToPortfolio("port_123", DashboardEvent.PORTFOLIO_UPDATE, {
portfolioId: "port_123",
totalValue: 15000,
change: 500,
changePercent: 3.45,
timestamp: new Date().toISOString(),
});| Code | Description |
|---|---|
AUTH_REQUIRED |
No authentication token provided |
AUTH_FAILED |
Token validation failed |
INVALID_TOKEN |
Token is malformed or expired |
WS_ERROR |
General WebSocket error |
INTERNAL_ERROR |
Server internal error |
UNKNOWN_ERROR |
Unknown error occurred |
WebSocket health can be checked via the health endpoint:
GET /api/v1/health/websocket
Response:
{
"status": "healthy",
"connections": {
"total": 50,
"active": 48,
"stale": 2
},
"lastCheck": "2024-01-15T10:30:00.000Z",
"uptime": 86400000
}Run WebSocket tests:
npm test -- --testPathPattern=websocketThe implementation supports stress testing with 1000 concurrent clients:
npm run test:stress -- --clients=1000 --expectedFailureRate=0.01Requirements:
- Max 1% failure rate
- All clients reconnect successfully with exponential backoff
- Events replayed correctly upon reconnection