- 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
233 lines
7.9 KiB
JavaScript
233 lines
7.9 KiB
JavaScript
const express = require('express');
|
|
const path = require('path');
|
|
const dotenv = require('dotenv');
|
|
|
|
// Load environment variables
|
|
const ENV = process.env.NODE_ENV || 'development';
|
|
dotenv.config({ path: `.env.${ENV}` });
|
|
|
|
const app = express();
|
|
const PORT = process.env.PORT || 3002;
|
|
|
|
// Disable Express poweredBy header for performance
|
|
app.disable('x-powered-by');
|
|
|
|
// Parse JSON bodies
|
|
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
|
|
let sseConnections = 0;
|
|
|
|
// Health check endpoint
|
|
app.get('/health', (req, res) => {
|
|
console.log('que pedos');
|
|
|
|
res.json({
|
|
status: 'healthy',
|
|
service: 'snatchgame-admin',
|
|
environment: ENV,
|
|
serverUrl: process.env.SERVER_URL
|
|
});
|
|
});
|
|
|
|
// API endpoint to get environment config for client
|
|
app.get('/api/config', (req, res) => {
|
|
res.json({
|
|
serverUrl: process.env.PUBLIC_SERVER_URL || process.env.SERVER_URL,
|
|
environment: ENV
|
|
});
|
|
});
|
|
|
|
// SSE endpoint for real-time updates
|
|
app.get('/api/sse', (req, res) => {
|
|
sseConnections++;
|
|
const connectionId = sseConnections;
|
|
console.log(`[SSE] New connection #${connectionId} established. Total: ${sseConnections}`);
|
|
|
|
// Optimized headers for better performance
|
|
res.writeHead(200, {
|
|
'Content-Type': 'text/event-stream',
|
|
'Cache-Control': 'no-cache, no-store, must-revalidate',
|
|
'Connection': 'keep-alive',
|
|
'Access-Control-Allow-Origin': '*',
|
|
'Access-Control-Allow-Headers': 'Cache-Control',
|
|
'X-Accel-Buffering': 'no', // Disable nginx buffering
|
|
'Content-Encoding': 'identity' // Disable compression
|
|
});
|
|
|
|
// Send initial connection message
|
|
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
|
|
const pollInterval = setInterval(async () => {
|
|
const startTime = Date.now();
|
|
try {
|
|
// Fetch game state from Colyseus server
|
|
const gameServerUrl = process.env.SERVER_URL || 'http://localhost:2567';
|
|
console.log(`[SSE] Fetching stats from: ${gameServerUrl}/api/admin/stats`);
|
|
|
|
const response = await fetch(`${gameServerUrl}/api/admin/stats`);
|
|
|
|
const fetchTime = Date.now() - startTime;
|
|
|
|
if (response.ok) {
|
|
const gameStats = await response.json();
|
|
console.log(`[SSE] Stats fetched successfully in ${fetchTime}ms`);
|
|
res.write(`data: ${JSON.stringify({
|
|
type: 'gameStats',
|
|
timestamp: new Date().toISOString(),
|
|
data: gameStats
|
|
})}\n\n`);
|
|
} else {
|
|
console.log(`[SSE] Stats fetch failed with status ${response.status} in ${fetchTime}ms`);
|
|
// Send error status if server is not reachable
|
|
res.write(`data: ${JSON.stringify({
|
|
type: 'error',
|
|
timestamp: new Date().toISOString(),
|
|
message: `Cannot connect to game server (${response.status})`
|
|
})}\n\n`);
|
|
}
|
|
} catch (error) {
|
|
const fetchTime = Date.now() - startTime;
|
|
console.error(`[SSE] Error fetching game stats in ${fetchTime}ms:`, error.message);
|
|
res.write(`data: ${JSON.stringify({
|
|
type: 'error',
|
|
timestamp: new Date().toISOString(),
|
|
message: `Error fetching game stats: ${error.message}`
|
|
})}\n\n`);
|
|
}
|
|
}, 250); // Poll every 250ms for faster updates
|
|
|
|
// Clean up on client disconnect
|
|
req.on('close', () => {
|
|
sseConnections--;
|
|
console.log(`[SSE] Connection #${connectionId} closed. Total: ${sseConnections}`);
|
|
clearInterval(pollInterval);
|
|
clearInterval(heartbeatInterval);
|
|
});
|
|
});
|
|
|
|
// Admin control endpoints - proxy to Colyseus server
|
|
|
|
// Kick player endpoint
|
|
app.post('/api/admin/kick-player', async (req, res) => {
|
|
try {
|
|
const gameServerUrl = process.env.SERVER_URL || 'http://localhost:2567';
|
|
const response = await fetch(`${gameServerUrl}/api/admin/kick-player`, {
|
|
method: 'POST',
|
|
headers: { 'Content-Type': 'application/json' },
|
|
body: JSON.stringify(req.body)
|
|
});
|
|
|
|
const result = await response.json();
|
|
res.json(result);
|
|
} catch (error) {
|
|
res.status(500).json({ success: false, message: 'Error communicating with game server' });
|
|
}
|
|
});
|
|
|
|
// Pause game endpoint
|
|
app.post('/api/admin/pause-game', async (req, res) => {
|
|
try {
|
|
const gameServerUrl = process.env.SERVER_URL || 'http://localhost:2567';
|
|
const response = await fetch(`${gameServerUrl}/api/admin/pause-game`, {
|
|
method: 'POST',
|
|
headers: { 'Content-Type': 'application/json' },
|
|
body: JSON.stringify(req.body)
|
|
});
|
|
|
|
const result = await response.json();
|
|
res.json(result);
|
|
} catch (error) {
|
|
res.status(500).json({ success: false, message: 'Error communicating with game server' });
|
|
}
|
|
});
|
|
|
|
// Resume game endpoint
|
|
app.post('/api/admin/resume-game', async (req, res) => {
|
|
try {
|
|
const gameServerUrl = process.env.SERVER_URL || 'http://localhost:2567';
|
|
const response = await fetch(`${gameServerUrl}/api/admin/resume-game`, {
|
|
method: 'POST',
|
|
headers: { 'Content-Type': 'application/json' },
|
|
body: JSON.stringify(req.body)
|
|
});
|
|
|
|
const result = await response.json();
|
|
res.json(result);
|
|
} catch (error) {
|
|
res.status(500).json({ success: false, message: 'Error communicating with game server' });
|
|
}
|
|
});
|
|
|
|
// Cancel game endpoint
|
|
app.post('/api/admin/cancel-game', async (req, res) => {
|
|
try {
|
|
const gameServerUrl = process.env.SERVER_URL || 'http://localhost:2567';
|
|
const response = await fetch(`${gameServerUrl}/api/admin/cancel-game`, {
|
|
method: 'POST',
|
|
headers: { 'Content-Type': 'application/json' },
|
|
body: JSON.stringify(req.body)
|
|
});
|
|
|
|
const result = await response.json();
|
|
res.json(result);
|
|
} catch (error) {
|
|
res.status(500).json({ success: false, message: 'Error communicating with game server' });
|
|
}
|
|
});
|
|
|
|
// Configure MIME types for modules
|
|
express.static.mime.define({'application/javascript': ['js', 'mjs']});
|
|
|
|
// Serve static files based on environment (AFTER API routes)
|
|
if (ENV === 'production') {
|
|
// Production: serve from dist first
|
|
app.use(express.static('dist'));
|
|
app.use(express.static('.'));
|
|
} else {
|
|
// Development: serve from current directory only
|
|
app.use(express.static('.'));
|
|
}
|
|
|
|
// Serve main HTML file for SPA routes based on environment
|
|
app.get('*', (req, res) => {
|
|
if (ENV === 'production') {
|
|
const distIndexPath = path.join(__dirname, 'dist', 'index.html');
|
|
const fs = require('fs');
|
|
if (fs.existsSync(distIndexPath)) {
|
|
res.sendFile(distIndexPath);
|
|
} else {
|
|
res.status(500).send('Production build not found. Run npm run build first.');
|
|
}
|
|
} else {
|
|
// Development: serve dev index.html
|
|
res.sendFile(path.join(__dirname, 'index.html'));
|
|
}
|
|
});
|
|
|
|
app.listen(PORT, () => {
|
|
const publicUrl = ENV === 'production' ? 'https://snatchgGameAdmin.interno.com' : `http://localhost:${PORT}`;
|
|
console.log(`
|
|
📊 SnatchGame Admin Dashboard
|
|
📱 Environment: ${ENV}
|
|
🌐 Server URL: ${publicUrl}
|
|
🔗 Game Server: ${process.env.SERVER_URL}
|
|
📡 SSE Endpoint: ${publicUrl}/api/sse
|
|
`);
|
|
}); |