Fix: make reconnect more robust and make spectate window re-openable

This commit is contained in:
jwinterm
2026-01-07 17:44:47 -05:00
parent f18fdf3802
commit 251ace4635
6 changed files with 102 additions and 12 deletions
+1 -1
View File
@@ -215,7 +215,7 @@ const InputHandler = {
} else {
// If not showing waiting screen, add helpful message to chat if missing address
if (addressRequired && !hasAddress) {
$('#messages').append($('<li style="color: #ffa500;">').text("⚠️ Please set a payout address before starting."));
$('#messages').append($('<li style="color: #ffa500;">').text("⚠️ Please set a payout address using the button below before starting."));
UI.scrollChat();
}
}
+12 -2
View File
@@ -27,9 +27,19 @@ class QueryValidator {
}
// Check for dangerous patterns in the query text itself
const upperText = text.toUpperCase();
for (const keyword of this.dangerousKeywords) {
if (upperText.includes(keyword)) {
// Use regex to avoid false positives (e.g., "DESCRIPTION" containing "SCRIPT")
// Alphanumeric keywords use word boundaries; others use literal match
let regex;
if (/^[A-Z0-9_]+$/i.test(keyword)) {
regex = new RegExp(`\\b${keyword}\\b`, 'i');
} else {
// Escape special characters for literal match
const escaped = keyword.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
regex = new RegExp(escaped, 'i');
}
if (regex.test(text)) {
// Allow legitimate DDL in migrations only
const stack = new Error().stack;
if (!stack.includes('migrations')) {
+16 -2
View File
@@ -7,13 +7,14 @@
const { normalizeError } = require('../utils/errors');
class PaymentHandlers {
constructor({ io, gameModeManager, walletService, debugManager, queueManager, broadcastManager }) {
constructor({ io, gameModeManager, walletService, debugManager, queueManager, broadcastManager, sessionManager }) {
this.io = io;
this.gameModeManager = gameModeManager;
this.walletService = walletService;
this.debugManager = debugManager;
this.queueManager = queueManager;
this.broadcastManager = broadcastManager;
this.sessionManager = sessionManager;
this.mempoolNotified = new Set();
this.paymentMonitors = new Map();
// Track pending payment metadata per socket (may reuse existing)
@@ -239,7 +240,20 @@ class PaymentHandlers {
socket.emit('payment_detected', { paymentId: paymentRequest.id, message: 'Payment detected in mempool! Adding you to the game queue...', amount: status.amount, confirmations: 0 });
const existingIdx = this.queueManager.getPlayerIndex(socket.id);
if (existingIdx === -1) {
this.queueManager.addPlayer({ serverId: socket.id, clientId: currentUser ? currentUser.clientId : null, paymentId: paymentRequest.id, requiresConfirmation: true, confirmed: false });
// Try to get DB userId from session
let userId = null;
if (this.sessionManager?.sessions?.has(socket.id)) {
userId = this.sessionManager.sessions.get(socket.id).id;
}
this.queueManager.addPlayer({
serverId: socket.id,
clientId: currentUser ? currentUser.clientId : null,
userId: userId,
paymentId: paymentRequest.id,
requiresConfirmation: true,
confirmed: false
});
}
socket.emit('queue_joined', { position: (existingIdx === -1 ? this.queueManager.getQueueLength() : existingIdx + 1), message: 'Payment received! Waiting for next block to start game...', currentBlock: this.debugManager.getCurrentBlockHeight ? this.debugManager.getCurrentBlockHeight() : null, nextBlock: this.debugManager.getCurrentBlockHeight ? this.debugManager.getCurrentBlockHeight() + 1 : null });
} else if (status.confirmed) {
+10
View File
@@ -73,10 +73,20 @@ class QueueHandler {
// Record the queue attempt
await this.rateLimiter.recordAttempt(socket.id, 'game:queue');
// Get session info to pass userId
let userId = null;
if (this.paymentHandlers?.gameModeManager?.getOrCreateUser) {
try {
const dbUser = await this.paymentHandlers.gameModeManager.getOrCreateUser(socket.id);
userId = dbUser.id;
} catch (e) {}
}
// Add to waiting queue (in free mode or already authorized paid mode)
this.queueManager.addPlayer({
serverId: socket.id,
clientId: currentUser.clientId,
userId: userId,
requiresConfirmation: false,
confirmed: true
});
+28 -3
View File
@@ -22,14 +22,39 @@ class QueueManager {
this._waitingPlayers = []; // internal queue
}
addPlayer({ serverId, clientId, entryTime = Date.now(), paymentId = null, requiresConfirmation = false, confirmed = true }) {
addPlayer({ serverId, clientId, userId = null, entryTime = Date.now(), paymentId = null, requiresConfirmation = false, confirmed = true }) {
if (this.getPlayerIndex(serverId) !== -1) return; // already queued
this._waitingPlayers.push({ serverId, clientId, entryTime, paymentId, requiresConfirmation, confirmed });
this._waitingPlayers.push({ serverId, clientId, userId, entryTime, paymentId, requiresConfirmation, confirmed });
if (this.CONSOLE_LOGGING) {
console.log(`[QueueManager] Added player ${serverId} (confirmed=${confirmed}, requiresConfirmation=${requiresConfirmation}). Queue length: ${this._waitingPlayers.length}`);
console.log(`[QueueManager] Added player ${serverId} (userId=${userId}, confirmed=${confirmed}, requiresConfirmation=${requiresConfirmation}). Queue length: ${this._waitingPlayers.length}`);
}
}
/**
* Update the serverId (socket.id) for a queued player when their session resumes
*/
updateSocketId(userId, newSocketId) {
const entry = this._waitingPlayers.find(p => p.userId === userId);
if (entry) {
if (this.CONSOLE_LOGGING) {
console.log(`[QueueManager] Updating socket ID for user ${userId}: ${entry.serverId} -> ${newSocketId}`);
}
entry.serverId = newSocketId;
return true;
}
return false;
}
/**
* Check if a player entry has persistent data that should be preserved on disconnect
*/
isValuableEntry(serverId) {
const idx = this.getPlayerIndex(serverId);
if (idx === -1) return false;
const entry = this._waitingPlayers[idx];
return !!(entry.paymentId || entry.userId);
}
removePlayer(serverId) {
const idx = this.getPlayerIndex(serverId);
if (idx !== -1) {
+35 -4
View File
@@ -98,7 +98,8 @@ class SocketHandlers {
walletService: this.walletService,
debugManager: this.debugManager,
queueManager: this.queueManager,
broadcastManager: this.broadcastManager
broadcastManager: this.broadcastManager,
sessionManager: this.sessionManager
});
// Address / payout handling encapsulated in AddressManager
@@ -341,6 +342,26 @@ class SocketHandlers {
const connectionResult = await this.connectionHandler.handleConnection(socket);
if (!connectionResult) return; // Connection was rejected or failed
const { sessionInfo } = connectionResult;
// Restore queue status if session resumed
if (sessionInfo && sessionInfo.resumed && sessionInfo.user && this.queueManager) {
const wasUpdated = this.queueManager.updateSocketId(sessionInfo.user.id, socket.id);
if (wasUpdated) {
const position = this.queueManager.getQueuePosition(socket.id);
if (position !== -1) {
this.broadcastManager.sendStatusUpdate(socket.id, 'queue',
`Welcome back! You are still in the queue at position ${position}.`);
socket.emit('queue_joined', {
position: position,
message: `Welcome back! You are still in queue.`,
currentBlock: this.debugManager.getCurrentBlockHeight ? this.debugManager.getCurrentBlockHeight() : null,
nextBlock: (this.debugManager.getCurrentBlockHeight ? this.debugManager.getCurrentBlockHeight() : 0) + 1
});
}
}
}
// Send game mode info to client
if (this.gameModeManager) {
socket.emit('game_mode_info', this.gameModeManager.getGameModeInfo());
@@ -428,7 +449,7 @@ class SocketHandlers {
try {
const dbUser = await this.gameModeManager.getOrCreateUser(socket.id);
if (!dbUser.payout_address) {
this.broadcastManager.sendStatusUpdate(socket.id, 'payment', '⚠️ Paste your payout address first, then type confirm. Payment request will appear automatically.');
this.broadcastManager.sendStatusUpdate(socket.id, 'payment', '⚠️ Use the "Manage Payout Address" button or paste your payout address here and type confirm. Payment request will appear automatically.');
return;
}
} catch (e) {
@@ -649,8 +670,18 @@ class SocketHandlers {
// Clean up active games
this.activeGames.delete(socket.id);
// Remove from queue manager (idempotent)
this.queueManager.removePlayer(socket.id);
// Remove from queue manager if not a "valuable" entry (persistent session or payment)
// This allows users who paid but disconnected to stay in the queue for session resumption
if (this.queueManager && typeof this.queueManager.removePlayer === 'function') {
if (this.queueManager.isValuableEntry && !this.queueManager.isValuableEntry(socket.id)) {
this.queueManager.removePlayer(socket.id);
} else if (this.debugManager.CONSOLE_LOGGING && this.queueManager.isValuableEntry && this.queueManager.isValuableEntry(socket.id)) {
console.log(`[SocketHandlers] Preserving queue entry for ${socket.id} (user has session/payment)`);
} else if (!this.queueManager.isValuableEntry) {
// Fallback for safety if method not found
this.queueManager.removePlayer(socket.id);
}
}
// Clear any pending address confirmations
if (this.chatHandler) {