fix: auto-cleanup orphaned arcade sessions without valid rooms
Fixes critical bug where users were redirected to non-existent games after room TTL deletion. This occurred because: 1. User creates arcade session in a room 2. Room expires via TTL cleanup 3. Session persists as orphan (roomId = null or points to deleted room) 4. useArcadeRedirect finds orphaned session 5. User redirected to /arcade/matching with no valid game state Changes: **Session validation (session-manager.ts)** - getArcadeSession() now validates room association - Auto-deletes sessions with no roomId - Auto-deletes sessions pointing to non-existent rooms - Returns undefined for orphaned sessions **Session creation (session-manager.ts, route.ts, socket-server.ts)** - createArcadeSession() now requires roomId parameter - Socket server checks for existing user rooms before creating new ones - Socket server auto-creates rooms when needed for backward compatibility - API route requires roomId in request body **Tests** - Added orphaned-session-cleanup.test.ts: Unit/integration tests - Added orphaned-session.e2e.test.ts: E2E regression tests - Updated existing tests to provide roomId - Tests cover TTL deletion, null roomId, and race conditions This ensures sessions are always tied to valid rooms and prevents orphaned session redirect loops. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
203
apps/web/__tests__/orphaned-session.e2e.test.ts
Normal file
203
apps/web/__tests__/orphaned-session.e2e.test.ts
Normal file
@@ -0,0 +1,203 @@
|
||||
import { afterEach, beforeEach, describe, expect, it } from 'vitest'
|
||||
import { eq } from 'drizzle-orm'
|
||||
import { db, schema } from '../src/db'
|
||||
import { createArcadeSession, getArcadeSession } from '../src/lib/arcade/session-manager'
|
||||
import { cleanupExpiredRooms, createRoom } from '../src/lib/arcade/room-manager'
|
||||
|
||||
/**
|
||||
* E2E Test: Orphaned Session After Room TTL Deletion
|
||||
*
|
||||
* This test simulates the exact scenario reported by the user:
|
||||
* 1. User creates a game session in a room
|
||||
* 2. Room expires via TTL cleanup
|
||||
* 3. User navigates to /arcade
|
||||
* 4. System should NOT redirect to the orphaned game
|
||||
* 5. User should see the arcade lobby normally
|
||||
*/
|
||||
describe('E2E: Orphaned Session Cleanup on Navigation', () => {
|
||||
const testUserId = 'e2e-user-id'
|
||||
const testGuestId = 'e2e-guest-id'
|
||||
let testRoomId: string
|
||||
|
||||
beforeEach(async () => {
|
||||
// Create test user (simulating new or returning visitor)
|
||||
await db
|
||||
.insert(schema.users)
|
||||
.values({
|
||||
id: testUserId,
|
||||
guestId: testGuestId,
|
||||
createdAt: new Date(),
|
||||
})
|
||||
.onConflictDoNothing()
|
||||
})
|
||||
|
||||
afterEach(async () => {
|
||||
// Clean up test data
|
||||
await db.delete(schema.arcadeSessions).where(eq(schema.arcadeSessions.userId, testUserId))
|
||||
await db.delete(schema.users).where(eq(schema.users.id, testUserId))
|
||||
if (testRoomId) {
|
||||
try {
|
||||
await db.delete(schema.arcadeRooms).where(eq(schema.arcadeRooms.id, testRoomId))
|
||||
} catch {
|
||||
// Room may already be deleted
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
it('should not redirect user to orphaned game after room TTL cleanup', async () => {
|
||||
// === SETUP PHASE ===
|
||||
// User creates or joins a room
|
||||
const room = await createRoom({
|
||||
name: 'My Game Room',
|
||||
createdBy: testGuestId,
|
||||
creatorName: 'Test Player',
|
||||
gameName: 'matching',
|
||||
gameConfig: { difficulty: 6, gameType: 'abacus-numeral', turnTimer: 30 },
|
||||
ttlMinutes: 1, // Short TTL for testing
|
||||
})
|
||||
testRoomId = room.id
|
||||
|
||||
// User starts a game session
|
||||
const session = await createArcadeSession({
|
||||
userId: testGuestId,
|
||||
gameName: 'matching',
|
||||
gameUrl: '/arcade/matching',
|
||||
initialState: {
|
||||
gamePhase: 'playing',
|
||||
cards: [],
|
||||
gameCards: [],
|
||||
flippedCards: [],
|
||||
matchedPairs: 0,
|
||||
totalPairs: 6,
|
||||
currentPlayer: 'player-1',
|
||||
difficulty: 6,
|
||||
gameType: 'abacus-numeral',
|
||||
turnTimer: 30,
|
||||
},
|
||||
activePlayers: ['player-1'],
|
||||
roomId: room.id,
|
||||
})
|
||||
|
||||
// Verify session was created
|
||||
expect(session).toBeDefined()
|
||||
expect(session.roomId).toBe(room.id)
|
||||
|
||||
// === TTL EXPIRATION PHASE ===
|
||||
// Simulate time passing - room's TTL expires
|
||||
// Set lastActivity to past so cleanup detects it
|
||||
await db
|
||||
.update(schema.arcadeRooms)
|
||||
.set({
|
||||
lastActivity: new Date(Date.now() - 2 * 60 * 1000), // 2 minutes ago
|
||||
})
|
||||
.where(eq(schema.arcadeRooms.id, room.id))
|
||||
|
||||
// Run cleanup (simulating background cleanup job)
|
||||
const deletedCount = await cleanupExpiredRooms()
|
||||
expect(deletedCount).toBeGreaterThan(0) // Room should be deleted
|
||||
|
||||
// === USER NAVIGATION PHASE ===
|
||||
// User navigates to /arcade (arcade lobby)
|
||||
// The useArcadeRedirect hook calls getArcadeSession to check for active session
|
||||
const activeSession = await getArcadeSession(testGuestId)
|
||||
|
||||
// === ASSERTION PHASE ===
|
||||
// Expected behavior: NO active session returned
|
||||
// This prevents redirect to /arcade/matching which would be broken
|
||||
expect(activeSession).toBeUndefined()
|
||||
|
||||
// Verify the orphaned session was cleaned up from database
|
||||
const [orphanedSessionCheck] = await db
|
||||
.select()
|
||||
.from(schema.arcadeSessions)
|
||||
.where(eq(schema.arcadeSessions.userId, testUserId))
|
||||
.limit(1)
|
||||
|
||||
expect(orphanedSessionCheck).toBeUndefined()
|
||||
})
|
||||
|
||||
it('should allow user to start new game after orphaned session cleanup', async () => {
|
||||
// === SETUP: Create and orphan a session ===
|
||||
const oldRoom = await createRoom({
|
||||
name: 'Old Room',
|
||||
createdBy: testGuestId,
|
||||
creatorName: 'Test Player',
|
||||
gameName: 'matching',
|
||||
gameConfig: { difficulty: 6 },
|
||||
ttlMinutes: 1,
|
||||
})
|
||||
|
||||
await createArcadeSession({
|
||||
userId: testGuestId,
|
||||
gameName: 'matching',
|
||||
gameUrl: '/arcade/matching',
|
||||
initialState: { gamePhase: 'setup' },
|
||||
activePlayers: ['player-1'],
|
||||
roomId: oldRoom.id,
|
||||
})
|
||||
|
||||
// Delete room (TTL cleanup)
|
||||
await db.delete(schema.arcadeRooms).where(eq(schema.arcadeRooms.id, oldRoom.id))
|
||||
|
||||
// === ACTION: User tries to access arcade ===
|
||||
const orphanedSession = await getArcadeSession(testGuestId)
|
||||
expect(orphanedSession).toBeUndefined() // Orphan cleaned up
|
||||
|
||||
// === ACTION: User creates new room and session ===
|
||||
const newRoom = await createRoom({
|
||||
name: 'New Room',
|
||||
createdBy: testGuestId,
|
||||
creatorName: 'Test Player',
|
||||
gameName: 'matching',
|
||||
gameConfig: { difficulty: 8 },
|
||||
ttlMinutes: 60,
|
||||
})
|
||||
testRoomId = newRoom.id
|
||||
|
||||
const newSession = await createArcadeSession({
|
||||
userId: testGuestId,
|
||||
gameName: 'matching',
|
||||
gameUrl: '/arcade/matching',
|
||||
initialState: { gamePhase: 'setup' },
|
||||
activePlayers: ['player-1', 'player-2'],
|
||||
roomId: newRoom.id,
|
||||
})
|
||||
|
||||
// === ASSERTION: New session works correctly ===
|
||||
expect(newSession).toBeDefined()
|
||||
expect(newSession.roomId).toBe(newRoom.id)
|
||||
|
||||
const activeSession = await getArcadeSession(testGuestId)
|
||||
expect(activeSession).toBeDefined()
|
||||
expect(activeSession?.roomId).toBe(newRoom.id)
|
||||
})
|
||||
|
||||
it('should handle race condition: getArcadeSession called while room is being deleted', async () => {
|
||||
// Create room and session
|
||||
const room = await createRoom({
|
||||
name: 'Race Condition Room',
|
||||
createdBy: testGuestId,
|
||||
creatorName: 'Test Player',
|
||||
gameName: 'matching',
|
||||
gameConfig: { difficulty: 6 },
|
||||
ttlMinutes: 60,
|
||||
})
|
||||
testRoomId = room.id
|
||||
|
||||
await createArcadeSession({
|
||||
userId: testGuestId,
|
||||
gameName: 'matching',
|
||||
gameUrl: '/arcade/matching',
|
||||
initialState: { gamePhase: 'setup' },
|
||||
activePlayers: ['player-1'],
|
||||
roomId: room.id,
|
||||
})
|
||||
|
||||
// Simulate race: delete room while getArcadeSession is checking
|
||||
await db.delete(schema.arcadeRooms).where(eq(schema.arcadeRooms.id, room.id))
|
||||
|
||||
// Should gracefully handle and return undefined
|
||||
const result = await getArcadeSession(testGuestId)
|
||||
expect(result).toBeUndefined()
|
||||
})
|
||||
})
|
||||
@@ -7,7 +7,9 @@ import {
|
||||
getArcadeSession,
|
||||
updateSessionActivity,
|
||||
} from './src/lib/arcade/session-manager'
|
||||
import type { GameMove } from './src/lib/arcade/validation'
|
||||
import { createRoom, getRoomById } from './src/lib/arcade/room-manager'
|
||||
import { getUserRooms } from './src/lib/arcade/room-membership'
|
||||
import type { GameMove, GameName } from './src/lib/arcade/validation'
|
||||
import { matchingGameValidator } from './src/lib/arcade/validation/MatchingGameValidator'
|
||||
|
||||
export function initializeSocketServer(httpServer: HTTPServer) {
|
||||
@@ -85,15 +87,52 @@ export function initializeSocketServer(httpServer: HTTPServer) {
|
||||
turnTimer: 30,
|
||||
})
|
||||
|
||||
// Check if user is already in a room for this game
|
||||
const userRoomIds = await getUserRooms(data.userId)
|
||||
let room = null
|
||||
|
||||
// Look for an existing active room for this game
|
||||
for (const roomId of userRoomIds) {
|
||||
const existingRoom = await getRoomById(roomId)
|
||||
if (
|
||||
existingRoom &&
|
||||
existingRoom.gameName === 'matching' &&
|
||||
existingRoom.status !== 'finished'
|
||||
) {
|
||||
room = existingRoom
|
||||
console.log('🏠 Using existing room:', room.code)
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
// If no suitable room exists, create a new one
|
||||
if (!room) {
|
||||
room = await createRoom({
|
||||
name: 'Auto-generated Room',
|
||||
createdBy: data.userId,
|
||||
creatorName: 'Player',
|
||||
gameName: 'matching' as GameName,
|
||||
gameConfig: {
|
||||
difficulty: 6,
|
||||
gameType: 'abacus-numeral',
|
||||
turnTimer: 30,
|
||||
},
|
||||
ttlMinutes: 60,
|
||||
})
|
||||
console.log('🏠 Created new room:', room.code)
|
||||
}
|
||||
|
||||
// Now create the session linked to the room
|
||||
await createArcadeSession({
|
||||
userId: data.userId,
|
||||
gameName: 'matching',
|
||||
gameUrl: '/arcade/matching',
|
||||
initialState,
|
||||
activePlayers,
|
||||
roomId: room.id,
|
||||
})
|
||||
|
||||
console.log('✅ Session created successfully')
|
||||
console.log('✅ Session created successfully with room association')
|
||||
|
||||
// Notify all connected clients about the new session
|
||||
const newSession = await getArcadeSession(data.userId)
|
||||
|
||||
@@ -47,10 +47,16 @@ export async function GET(request: NextRequest) {
|
||||
export async function POST(request: NextRequest) {
|
||||
try {
|
||||
const body = await request.json()
|
||||
const { userId, gameName, gameUrl, initialState, activePlayers } = body
|
||||
const { userId, gameName, gameUrl, initialState, activePlayers, roomId } = body
|
||||
|
||||
if (!userId || !gameName || !gameUrl || !initialState || !activePlayers) {
|
||||
return NextResponse.json({ error: 'Missing required fields' }, { status: 400 })
|
||||
if (!userId || !gameName || !gameUrl || !initialState || !activePlayers || !roomId) {
|
||||
return NextResponse.json(
|
||||
{
|
||||
error:
|
||||
'Missing required fields (userId, gameName, gameUrl, initialState, activePlayers, roomId)',
|
||||
},
|
||||
{ status: 400 }
|
||||
)
|
||||
}
|
||||
|
||||
const session = await createArcadeSession({
|
||||
@@ -59,6 +65,7 @@ export async function POST(request: NextRequest) {
|
||||
gameUrl,
|
||||
initialState,
|
||||
activePlayers,
|
||||
roomId,
|
||||
})
|
||||
|
||||
return NextResponse.json({
|
||||
|
||||
@@ -8,6 +8,7 @@ import {
|
||||
deleteArcadeSession,
|
||||
getArcadeSession,
|
||||
} from '../session-manager'
|
||||
import { createRoom, deleteRoom } from '../room-manager'
|
||||
|
||||
/**
|
||||
* Integration test for the full arcade session flow
|
||||
@@ -16,6 +17,7 @@ import {
|
||||
describe('Arcade Session Integration', () => {
|
||||
const testUserId = 'integration-test-user'
|
||||
const testGuestId = 'integration-test-guest'
|
||||
let testRoomId: string
|
||||
|
||||
beforeEach(async () => {
|
||||
// Create test user
|
||||
@@ -27,11 +29,25 @@ describe('Arcade Session Integration', () => {
|
||||
createdAt: new Date(),
|
||||
})
|
||||
.onConflictDoNothing()
|
||||
|
||||
// Create test room
|
||||
const room = await createRoom({
|
||||
name: 'Test Room',
|
||||
createdBy: testGuestId,
|
||||
creatorName: 'Test User',
|
||||
gameName: 'matching',
|
||||
gameConfig: { difficulty: 6, gameType: 'abacus-numeral', turnTimer: 30 },
|
||||
ttlMinutes: 60,
|
||||
})
|
||||
testRoomId = room.id
|
||||
})
|
||||
|
||||
afterEach(async () => {
|
||||
// Clean up
|
||||
await deleteArcadeSession(testUserId)
|
||||
await deleteArcadeSession(testGuestId)
|
||||
if (testRoomId) {
|
||||
await deleteRoom(testRoomId)
|
||||
}
|
||||
await db.delete(schema.users).where(eq(schema.users.id, testUserId))
|
||||
})
|
||||
|
||||
@@ -63,11 +79,12 @@ describe('Arcade Session Integration', () => {
|
||||
}
|
||||
|
||||
const session = await createArcadeSession({
|
||||
userId: testUserId,
|
||||
userId: testGuestId,
|
||||
gameName: 'matching',
|
||||
gameUrl: '/arcade/matching',
|
||||
initialState,
|
||||
activePlayers: ['1'],
|
||||
roomId: testRoomId,
|
||||
})
|
||||
|
||||
expect(session).toBeDefined()
|
||||
@@ -165,11 +182,12 @@ describe('Arcade Session Integration', () => {
|
||||
}
|
||||
|
||||
await createArcadeSession({
|
||||
userId: testUserId,
|
||||
userId: testGuestId,
|
||||
gameName: 'matching',
|
||||
gameUrl: '/arcade/matching',
|
||||
initialState: playingState,
|
||||
activePlayers: ['1'],
|
||||
roomId: testRoomId,
|
||||
})
|
||||
|
||||
// First move: flip card 1
|
||||
|
||||
@@ -0,0 +1,199 @@
|
||||
import { eq } from 'drizzle-orm'
|
||||
import { afterEach, beforeEach, describe, expect, it } from 'vitest'
|
||||
import { db, schema } from '@/db'
|
||||
import { createArcadeSession, deleteArcadeSession, getArcadeSession } from '../session-manager'
|
||||
import { createRoom, deleteRoom } from '../room-manager'
|
||||
|
||||
/**
|
||||
* Integration tests for orphaned session cleanup
|
||||
*
|
||||
* These tests ensure that sessions without valid rooms are properly
|
||||
* cleaned up to prevent the bug where users get redirected to
|
||||
* non-existent games when rooms have been TTL deleted.
|
||||
*/
|
||||
describe('Orphaned Session Cleanup', () => {
|
||||
const testUserId = 'orphan-test-user-id'
|
||||
const testGuestId = 'orphan-test-guest-id'
|
||||
let testRoomId: string
|
||||
|
||||
beforeEach(async () => {
|
||||
// Create test user
|
||||
await db
|
||||
.insert(schema.users)
|
||||
.values({
|
||||
id: testUserId,
|
||||
guestId: testGuestId,
|
||||
createdAt: new Date(),
|
||||
})
|
||||
.onConflictDoNothing()
|
||||
|
||||
// Create test room
|
||||
const room = await createRoom({
|
||||
name: 'Orphan Test Room',
|
||||
createdBy: testGuestId,
|
||||
creatorName: 'Test User',
|
||||
gameName: 'matching',
|
||||
gameConfig: { difficulty: 6, gameType: 'abacus-numeral', turnTimer: 30 },
|
||||
ttlMinutes: 60,
|
||||
})
|
||||
testRoomId = room.id
|
||||
})
|
||||
|
||||
afterEach(async () => {
|
||||
// Clean up
|
||||
await deleteArcadeSession(testGuestId)
|
||||
if (testRoomId) {
|
||||
try {
|
||||
await deleteRoom(testRoomId)
|
||||
} catch {
|
||||
// Room may have been deleted in test
|
||||
}
|
||||
}
|
||||
await db.delete(schema.users).where(eq(schema.users.id, testUserId))
|
||||
})
|
||||
|
||||
it('should return undefined when session has no roomId', async () => {
|
||||
// Create a session with a valid room
|
||||
const session = await createArcadeSession({
|
||||
userId: testGuestId,
|
||||
gameName: 'matching',
|
||||
gameUrl: '/arcade/matching',
|
||||
initialState: { gamePhase: 'setup' },
|
||||
activePlayers: ['player-1'],
|
||||
roomId: testRoomId,
|
||||
})
|
||||
|
||||
expect(session).toBeDefined()
|
||||
expect(session.roomId).toBe(testRoomId)
|
||||
|
||||
// Manually set roomId to null to simulate orphaned session
|
||||
await db
|
||||
.update(schema.arcadeSessions)
|
||||
.set({ roomId: null })
|
||||
.where(eq(schema.arcadeSessions.userId, testUserId))
|
||||
|
||||
// Getting the session should auto-delete it and return undefined
|
||||
const result = await getArcadeSession(testGuestId)
|
||||
expect(result).toBeUndefined()
|
||||
|
||||
// Verify session was actually deleted
|
||||
const [directCheck] = await db
|
||||
.select()
|
||||
.from(schema.arcadeSessions)
|
||||
.where(eq(schema.arcadeSessions.userId, testUserId))
|
||||
.limit(1)
|
||||
|
||||
expect(directCheck).toBeUndefined()
|
||||
})
|
||||
|
||||
it('should return undefined when session room has been deleted', async () => {
|
||||
// Create a session with a valid room
|
||||
const session = await createArcadeSession({
|
||||
userId: testGuestId,
|
||||
gameName: 'matching',
|
||||
gameUrl: '/arcade/matching',
|
||||
initialState: { gamePhase: 'setup' },
|
||||
activePlayers: ['player-1'],
|
||||
roomId: testRoomId,
|
||||
})
|
||||
|
||||
expect(session).toBeDefined()
|
||||
expect(session.roomId).toBe(testRoomId)
|
||||
|
||||
// Delete the room (simulating TTL expiration)
|
||||
await deleteRoom(testRoomId)
|
||||
|
||||
// Getting the session should detect missing room and auto-delete
|
||||
const result = await getArcadeSession(testGuestId)
|
||||
expect(result).toBeUndefined()
|
||||
|
||||
// Verify session was actually deleted
|
||||
const [directCheck] = await db
|
||||
.select()
|
||||
.from(schema.arcadeSessions)
|
||||
.where(eq(schema.arcadeSessions.userId, testUserId))
|
||||
.limit(1)
|
||||
|
||||
expect(directCheck).toBeUndefined()
|
||||
})
|
||||
|
||||
it('should return valid session when room exists', async () => {
|
||||
// Create a session with a valid room
|
||||
const session = await createArcadeSession({
|
||||
userId: testGuestId,
|
||||
gameName: 'matching',
|
||||
gameUrl: '/arcade/matching',
|
||||
initialState: { gamePhase: 'setup' },
|
||||
activePlayers: ['player-1'],
|
||||
roomId: testRoomId,
|
||||
})
|
||||
|
||||
expect(session).toBeDefined()
|
||||
|
||||
// Getting the session should work fine when room exists
|
||||
const result = await getArcadeSession(testGuestId)
|
||||
expect(result).toBeDefined()
|
||||
expect(result?.roomId).toBe(testRoomId)
|
||||
expect(result?.currentGame).toBe('matching')
|
||||
})
|
||||
|
||||
it('should handle multiple getArcadeSession calls idempotently', async () => {
|
||||
// Create a session with a valid room
|
||||
await createArcadeSession({
|
||||
userId: testGuestId,
|
||||
gameName: 'matching',
|
||||
gameUrl: '/arcade/matching',
|
||||
initialState: { gamePhase: 'setup' },
|
||||
activePlayers: ['player-1'],
|
||||
roomId: testRoomId,
|
||||
})
|
||||
|
||||
// Delete the room
|
||||
await deleteRoom(testRoomId)
|
||||
|
||||
// Multiple calls should all return undefined and not error
|
||||
const result1 = await getArcadeSession(testGuestId)
|
||||
const result2 = await getArcadeSession(testGuestId)
|
||||
const result3 = await getArcadeSession(testGuestId)
|
||||
|
||||
expect(result1).toBeUndefined()
|
||||
expect(result2).toBeUndefined()
|
||||
expect(result3).toBeUndefined()
|
||||
})
|
||||
|
||||
it('should prevent orphaned sessions from causing redirect loops', async () => {
|
||||
/**
|
||||
* Regression test for the specific bug:
|
||||
* - Room gets TTL deleted
|
||||
* - Session persists with null/invalid roomId
|
||||
* - User visits /arcade
|
||||
* - useArcadeRedirect finds the orphaned session
|
||||
* - User gets redirected to /arcade/matching
|
||||
* - But there's no valid game to play
|
||||
*
|
||||
* Fix: getArcadeSession should auto-delete orphaned sessions
|
||||
*/
|
||||
|
||||
// 1. Create session with room
|
||||
await createArcadeSession({
|
||||
userId: testGuestId,
|
||||
gameName: 'matching',
|
||||
gameUrl: '/arcade/matching',
|
||||
initialState: { gamePhase: 'setup' },
|
||||
activePlayers: ['player-1'],
|
||||
roomId: testRoomId,
|
||||
})
|
||||
|
||||
// 2. Room gets TTL deleted
|
||||
await deleteRoom(testRoomId)
|
||||
|
||||
// 3. User's client checks for active session (like useArcadeRedirect does)
|
||||
const activeSession = await getArcadeSession(testGuestId)
|
||||
|
||||
// 4. Should return undefined, preventing redirect
|
||||
expect(activeSession).toBeUndefined()
|
||||
|
||||
// 5. User can now proceed to arcade lobby normally
|
||||
// (no redirect to non-existent game)
|
||||
})
|
||||
})
|
||||
@@ -104,6 +104,7 @@ describe('session-manager', () => {
|
||||
gameUrl: '/arcade/matching',
|
||||
initialState: {},
|
||||
activePlayers: ['1'],
|
||||
roomId: 'test-room-id',
|
||||
})
|
||||
|
||||
// Verify user lookup by guestId
|
||||
@@ -160,6 +161,7 @@ describe('session-manager', () => {
|
||||
gameUrl: '/arcade/matching',
|
||||
initialState: {},
|
||||
activePlayers: ['1'],
|
||||
roomId: 'test-room-id',
|
||||
})
|
||||
|
||||
// Verify user was created
|
||||
|
||||
@@ -13,6 +13,7 @@ export interface CreateSessionOptions {
|
||||
gameUrl: string
|
||||
initialState: unknown
|
||||
activePlayers: string[] // Player IDs (UUIDs)
|
||||
roomId: string // Required - sessions must be associated with a room
|
||||
}
|
||||
|
||||
export interface SessionUpdateResult {
|
||||
@@ -71,6 +72,7 @@ export async function createArcadeSession(
|
||||
gameUrl: options.gameUrl,
|
||||
gameState: options.initialState as any,
|
||||
activePlayers: options.activePlayers as any,
|
||||
roomId: options.roomId, // Associate session with room
|
||||
startedAt: now,
|
||||
lastActivityAt: now,
|
||||
expiresAt,
|
||||
@@ -96,8 +98,29 @@ export async function getArcadeSession(guestId: string): Promise<schema.ArcadeSe
|
||||
.where(eq(schema.arcadeSessions.userId, userId))
|
||||
.limit(1)
|
||||
|
||||
if (!session) return undefined
|
||||
|
||||
// Check if session has expired
|
||||
if (session && session.expiresAt < new Date()) {
|
||||
if (session.expiresAt < new Date()) {
|
||||
await deleteArcadeSession(guestId)
|
||||
return undefined
|
||||
}
|
||||
|
||||
// Check if session has a valid room association
|
||||
// Sessions without rooms are orphaned and should be cleaned up
|
||||
if (!session.roomId) {
|
||||
console.log('[Session Manager] Deleting orphaned session without room:', session.userId)
|
||||
await deleteArcadeSession(guestId)
|
||||
return undefined
|
||||
}
|
||||
|
||||
// Verify the room still exists
|
||||
const room = await db.query.arcadeRooms.findFirst({
|
||||
where: eq(schema.arcadeRooms.id, session.roomId),
|
||||
})
|
||||
|
||||
if (!room) {
|
||||
console.log('[Session Manager] Deleting session with non-existent room:', session.roomId)
|
||||
await deleteArcadeSession(guestId)
|
||||
return undefined
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user