import express from "express"; import { createServer } from "http"; import { Server } from "socket.io"; import path from "path"; import https from "https"; async function startServer() { const app = express(); const httpServer = createServer(app); const io = new Server(httpServer, { pingTimeout: 60000, // 60 seconds to wait for pong pingInterval: 25000, // ping every 25 seconds to keep alive cors: { origin: "*", methods: ["GET", "POST"] }, }); // Ping every 14 minutes to prevent spin-down (if URL is provided) const APP_URL = process.env.APP_URL || process.env.RAILWAY_STATIC_URL; if (APP_URL) { setInterval(() => { const url = APP_URL.startsWith('http') ? APP_URL : `https://${APP_URL}`; https.get(url, (res) => { console.log(`Keep-alive ping to ${url} status:`, res.statusCode); }).on('error', (err) => { console.error(`Keep-alive ping to ${url} error:`, err.message); }); }, 840000); } const PORT = parseInt(process.env.PORT || "3000"); // Room state management (minimal, just to relay) const rooms = new Map(); io.on("connection", (socket) => { console.log("User connected:", socket.id); socket.on("join", ({ roomId, username, maxPlayers, playerId }) => { if (!rooms.has(roomId)) { rooms.set(roomId, { hostId: socket.id, hostPlayerId: playerId, players: [], maxPlayers: maxPlayers || 10 }); } const room = rooms.get(roomId); const existingPlayer = room.players.find(p => p.playerId === playerId); if (existingPlayer) { // Reconnection existingPlayer.id = socket.id; existingPlayer.name = username || existingPlayer.name; socket.join(roomId); // If they were host, update hostId to new socket.id if (room.hostPlayerId === playerId) { room.hostId = socket.id; } } else { // New join if (room.players.length >= room.maxPlayers) { socket.emit("error_message", "Room is full"); return; } socket.join(roomId); room.players.push({ id: socket.id, playerId, name: username }); } io.to(roomId).emit("lobby_update", { roomId, players: room.players, hostId: room.hostId, maxPlayers: room.maxPlayers }); }); socket.on("start_game", ({ roomId, initialState }) => { io.to(roomId).emit("start_game", initialState); }); socket.on("action", ({ roomId, action }) => { const room = rooms.get(roomId); if (room) { io.to(room.hostId).emit("action_received", { playerId: socket.id, action }); } }); socket.on("state_update", ({ roomId, state }) => { io.to(roomId).emit("state_update", state); }); socket.on("disconnect", () => { console.log("User disconnected:", socket.id); // We don't immediately remove players to allow reconnection. // We only clean up if the room becomes completely empty or after a long timeout. // For this simple implementation, we'll just leave them in the room. // A more robust version would mark them as 'offline'. }); }); if (process.env.NODE_ENV !== "production") { const { createServer: createViteServer } = await import("vite"); const vite = await createViteServer({ server: { middlewareMode: true }, appType: "spa", }); app.use(vite.middlewares); } else { const distPath = path.join(process.cwd(), "dist"); app.use(express.static(distPath)); app.get("*", (req, res) => { const indexPath = path.join(distPath, "index.html"); res.sendFile(indexPath, (err) => { if (err) { res.status(500).send("Build artifacts not found. Please run 'npm run build' first."); } }); }); } // Health check route app.get("/api/health", (req, res) => { res.json({ status: "ok", mode: process.env.NODE_ENV || 'development' }); }); httpServer.listen(PORT, "0.0.0.0", () => { console.log(`Server running on port ${PORT} in ${process.env.NODE_ENV || 'development'} mode`); }); } startServer();