perf: Optimize SSE communication between browser and container
- Add anti-buffering headers (X-Accel-Buffering, Content-Encoding) - Reduce polling interval from 500ms to 250ms for faster updates - Add heartbeat mechanism to keep connection alive - Implement auto-reconnection on client side - Disable Express response buffering for SSE endpoints - Skip empty heartbeat messages on client - Improve error handling and logging
This commit is contained in:
@@ -9,9 +9,22 @@ dotenv.config({ path: `.env.${ENV}` });
|
|||||||
const app = express();
|
const app = express();
|
||||||
const PORT = process.env.PORT || 3002;
|
const PORT = process.env.PORT || 3002;
|
||||||
|
|
||||||
|
// Disable Express poweredBy header for performance
|
||||||
|
app.disable('x-powered-by');
|
||||||
|
|
||||||
// Parse JSON bodies
|
// Parse JSON bodies
|
||||||
app.use(express.json());
|
app.use(express.json());
|
||||||
|
|
||||||
|
// Optimize Express for SSE performance
|
||||||
|
app.use((req, res, next) => {
|
||||||
|
// Disable buffering for SSE endpoints
|
||||||
|
if (req.path === '/api/sse') {
|
||||||
|
res.setHeader('Cache-Control', 'no-cache');
|
||||||
|
res.setHeader('Connection', 'keep-alive');
|
||||||
|
}
|
||||||
|
next();
|
||||||
|
});
|
||||||
|
|
||||||
// Track SSE connections
|
// Track SSE connections
|
||||||
let sseConnections = 0;
|
let sseConnections = 0;
|
||||||
|
|
||||||
@@ -41,16 +54,24 @@ app.get('/api/sse', (req, res) => {
|
|||||||
const connectionId = sseConnections;
|
const connectionId = sseConnections;
|
||||||
console.log(`[SSE] New connection #${connectionId} established. Total: ${sseConnections}`);
|
console.log(`[SSE] New connection #${connectionId} established. Total: ${sseConnections}`);
|
||||||
|
|
||||||
|
// Optimized headers for better performance
|
||||||
res.writeHead(200, {
|
res.writeHead(200, {
|
||||||
'Content-Type': 'text/event-stream',
|
'Content-Type': 'text/event-stream',
|
||||||
'Cache-Control': 'no-cache',
|
'Cache-Control': 'no-cache, no-store, must-revalidate',
|
||||||
'Connection': 'keep-alive',
|
'Connection': 'keep-alive',
|
||||||
'Access-Control-Allow-Origin': '*',
|
'Access-Control-Allow-Origin': '*',
|
||||||
'Access-Control-Allow-Headers': 'Cache-Control'
|
'Access-Control-Allow-Headers': 'Cache-Control',
|
||||||
|
'X-Accel-Buffering': 'no', // Disable nginx buffering
|
||||||
|
'Content-Encoding': 'identity' // Disable compression
|
||||||
});
|
});
|
||||||
|
|
||||||
// Send initial connection message
|
// Send initial connection message
|
||||||
res.write('data: {"type": "connected", "message": "SSE connection established"}\n\n');
|
res.write('data: {"type": "connected", "message": "SSE connection established"}\n\n');
|
||||||
|
|
||||||
|
// Send keepalive heartbeat every 30 seconds
|
||||||
|
const heartbeatInterval = setInterval(() => {
|
||||||
|
res.write(': heartbeat\n\n');
|
||||||
|
}, 30000);
|
||||||
|
|
||||||
// Set up polling interval for game state updates
|
// Set up polling interval for game state updates
|
||||||
const pollInterval = setInterval(async () => {
|
const pollInterval = setInterval(async () => {
|
||||||
@@ -90,13 +111,14 @@ app.get('/api/sse', (req, res) => {
|
|||||||
message: `Error fetching game stats: ${error.message}`
|
message: `Error fetching game stats: ${error.message}`
|
||||||
})}\n\n`);
|
})}\n\n`);
|
||||||
}
|
}
|
||||||
}, 500); // Poll every 500ms
|
}, 250); // Poll every 250ms for faster updates
|
||||||
|
|
||||||
// Clean up on client disconnect
|
// Clean up on client disconnect
|
||||||
req.on('close', () => {
|
req.on('close', () => {
|
||||||
sseConnections--;
|
sseConnections--;
|
||||||
console.log(`[SSE] Connection #${connectionId} closed. Total: ${sseConnections}`);
|
console.log(`[SSE] Connection #${connectionId} closed. Total: ${sseConnections}`);
|
||||||
clearInterval(pollInterval);
|
clearInterval(pollInterval);
|
||||||
|
clearInterval(heartbeatInterval);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -47,6 +47,8 @@ class AdminService {
|
|||||||
await this.initializeServerUrl()
|
await this.initializeServerUrl()
|
||||||
|
|
||||||
this.callback = callback
|
this.callback = callback
|
||||||
|
|
||||||
|
// Configure EventSource with better error handling
|
||||||
this.eventSource = new EventSource('/api/sse')
|
this.eventSource = new EventSource('/api/sse')
|
||||||
|
|
||||||
this.eventSource.onopen = () => {
|
this.eventSource.onopen = () => {
|
||||||
@@ -57,11 +59,14 @@ class AdminService {
|
|||||||
|
|
||||||
this.eventSource.onmessage = (event) => {
|
this.eventSource.onmessage = (event) => {
|
||||||
try {
|
try {
|
||||||
|
// Skip heartbeat messages
|
||||||
|
if (event.data.trim() === '') return
|
||||||
|
|
||||||
const data = JSON.parse(event.data)
|
const data = JSON.parse(event.data)
|
||||||
console.log('[AdminService] SSE message received:', data.type, data.timestamp)
|
console.log('[AdminService] SSE message received:', data.type, data.timestamp)
|
||||||
this.callback?.(data)
|
this.callback?.(data)
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('[AdminService] Error parsing SSE message:', error)
|
console.error('[AdminService] Error parsing SSE message:', error, 'Raw data:', event.data)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -69,6 +74,14 @@ class AdminService {
|
|||||||
console.error('[AdminService] SSE connection error:', error)
|
console.error('[AdminService] SSE connection error:', error)
|
||||||
this.isConnected = false
|
this.isConnected = false
|
||||||
this.connectionCallback?.(false)
|
this.connectionCallback?.(false)
|
||||||
|
|
||||||
|
// Auto-reconnect after 5 seconds
|
||||||
|
setTimeout(() => {
|
||||||
|
if (!this.isConnected) {
|
||||||
|
console.log('[AdminService] Attempting to reconnect...')
|
||||||
|
this.connect(this.callback!)
|
||||||
|
}
|
||||||
|
}, 5000)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user