Commit f5079d8b authored by Mahmoud Aglan's avatar Mahmoud Aglan

fix: poll concurrency guard, room code retry, join race, domino draw debounce

Phase 1.6: add pollInFlight guard to match-session.js so overlapping polls
are skipped when server is slow.

Phase 2.5: room code generation retries up to 5 times on collision.

Phase 2.6: join_room uses service key, checks if room is full, prevents
double-join, and auto-starts when player count is met.

Phase 3.1: dealAndSyncToServer retries on failure with toast notification.

Phase 3.3: drawFromBoneyard syncs once after all tiles drawn instead of
firing N parallel requests.

Phase 3.6: check-invites uses PostgREST cs filter on JSONB players array
instead of scanning all waiting matches.
Co-Authored-By: 's avatarClaude Opus 4.6 <noreply@anthropic.com>
parent 3a5fdc5c
......@@ -450,9 +450,10 @@ if ($method === 'POST') {
}
}
// Check domino_matches table
// Check domino_matches table (filter by player at DB level)
$dominoInvites = $sdb->get('domino_matches', [
'status' => 'eq.waiting',
'players' => 'cs.["' . $userId . '"]',
'select' => 'id,players,game_state,created_at',
'order' => 'created_at.desc',
'limit' => 5
......@@ -479,9 +480,10 @@ if ($method === 'POST') {
}
}
// Check ludo_matches table
// Check ludo_matches table (filter by player at DB level)
$ludoInvites = $sdb->get('ludo_matches', [
'status' => 'eq.waiting',
'players' => 'cs.["' . $userId . '"]',
'select' => 'id,players,game_state,created_at',
'order' => 'created_at.desc',
'limit' => 5
......
......@@ -18,27 +18,44 @@ switch ($action) {
case 'create_room':
$db = supabase($token);
$table = $gameKey === 'domino' ? 'domino_matches' : 'ludo_matches';
$code = strtoupper(substr(bin2hex(random_bytes(3)), 0, 6));
$result = $db->insert($table, [
'room_code' => $code,
'status' => 'waiting',
'players' => json_encode([$userId]),
'created_by' => $userId
]);
$result = null;
$code = '';
for ($attempt = 0; $attempt < 5; $attempt++) {
$code = strtoupper(substr(bin2hex(random_bytes(3)), 0, 6));
$existing = $db->get($table, ['room_code' => 'eq.' . $code, 'status' => 'eq.waiting', 'limit' => 1]);
if (!empty($existing) && !isset($existing['error'])) continue;
$result = $db->insert($table, [
'room_code' => $code,
'status' => 'waiting',
'players' => json_encode([$userId]),
'created_by' => $userId
]);
break;
}
if (!$result) jsonError('Failed to generate unique room code', 500);
jsonResponse(['room_code' => $code, 'match' => $result[0] ?? $result]);
break;
case 'join_room':
$roomCode = $input['room_code'] ?? '';
if (!$roomCode) jsonError('room_code required');
$db = supabase($token);
$sdb = supabaseService();
$table = $gameKey === 'domino' ? 'domino_matches' : 'ludo_matches';
$match = $db->getOne($table, ['room_code' => 'eq.' . $roomCode, 'status' => 'eq.waiting']);
if (!$match) jsonError('Room not found', 404);
$match = $sdb->get($table, ['room_code' => 'eq.' . $roomCode, 'status' => 'eq.waiting', 'select' => 'id,players,player_count', 'limit' => 1]);
if (!is_array($match) || empty($match) || isset($match['error'])) jsonError('Room not found', 404);
$match = $match[0];
$players = is_array($match['players']) ? $match['players'] : json_decode($match['players'] ?? '[]', true);
if (in_array($userId, $players)) {
jsonResponse(['success' => true, 'match_id' => $match['id']]);
break;
}
$maxPlayers = $match['player_count'] ?? ($gameKey === 'domino' ? 2 : 4);
if (count($players) >= $maxPlayers) jsonError('Room is full', 409);
$players = json_decode($match['players'] ?? '[]', true);
$players[] = $userId;
$db->update($table, ['players' => json_encode($players)], ['id' => 'eq.' . $match['id']]);
$newStatus = count($players) >= $maxPlayers ? 'in_progress' : 'waiting';
$sdb->update($table, ['players' => $players, 'status' => $newStatus], ['id' => 'eq.' . $match['id']]);
jsonResponse(['success' => true, 'match_id' => $match['id']]);
break;
......
......@@ -60,6 +60,7 @@ export function destroy() {
}
localStorage.removeItem('el3ab_active_match');
currentSession = null;
pollInFlight = false;
}
// Check if player has an active match to resume (after tab refresh)
......@@ -88,12 +89,16 @@ export function markOpponentActive() {
}
// ===== POLLING =====
let pollInFlight = false;
function startPolling() {
if (!currentSession) return;
currentSession.pollTimer = setInterval(async () => {
if (!currentSession || !currentSession.isActive) return;
if (currentSession.isBackground) return; // Don't poll when tab is hidden
if (currentSession.isBackground) return;
if (pollInFlight) return;
pollInFlight = true;
try {
const endpoint = currentSession.gameType === 'ludo' ? 'ludo-match.php' : currentSession.gameType === 'domino' ? 'domino-match.php' : currentSession.gameType === 'backgammon' ? 'backgammon-match.php' : 'game.php';
const data = await net.post(endpoint, {
......@@ -103,25 +108,23 @@ function startPolling() {
if (!data || data.error) return;
// Connection is alive
const wasDisconnected = (Date.now() - currentSession.lastServerPing) > 10000;
currentSession.lastServerPing = Date.now();
if (wasDisconnected) currentSession.onConnectionRestored?.();
// Handle server-detected abandonment
if (data.status === 'abandoned') {
currentSession.onOpponentAbandon?.();
return;
}
// Notify about opponent's move (includes _turn_timed_out flags)
currentSession.onOpponentMove?.(data);
} catch (e) {
// Network error
if (Date.now() - currentSession.lastServerPing > 10000) {
currentSession.onConnectionLost?.();
}
} finally {
pollInFlight = false;
}
}, POLL_INTERVAL);
}
......
......@@ -392,7 +392,11 @@ async function dealAndSyncToServer(el, matchId) {
dealt: true,
})
});
} catch (e) {}
} catch (e) {
console.error('[domino] deal sync failed:', e);
bus.emit('toast', { text: 'Sync error — retrying...', duration: 2000 });
setTimeout(() => dealAndSyncToServer(el, matchId), 2000);
}
}
async function fetchOpponentProfile(el) {
......@@ -840,12 +844,12 @@ async function drawFromBoneyard(el) {
state.hands[state.myPlayerIndex].push(tile);
audio.play('click');
juice.hapticLight?.();
syncDrawToServer();
updateUI(el);
refreshHand();
await new Promise(r => setTimeout(r, 400));
}
syncDrawToServer();
state.drawing = false;
if (!rules.hasValidMove(state.hands[state.myPlayerIndex], state.leftEnd, state.rightEnd) && state.boneyard.length === 0) {
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment