fix: prevent duplicate arcade sessions per room
**Root Cause:** Database schema had `user_id` as PRIMARY KEY for `arcade_sessions`, allowing multiple sessions for the same `roomId`. When two users joined the same room, each created their own session, causing them to see completely different games instead of sharing one session. **Database Changes:** - Changed PRIMARY KEY from `user_id` to `room_id` - Now enforces one session per room at database level - Updated all queries to use `room_id` as primary lookup key **Code Changes:** - Updated `createArcadeSession()` to check for existing room session - Added error handling for race conditions (UNIQUE constraint failures) - Modified `applyGameMove()`, `deleteArcadeSession()`, and `updateSessionActivity()` to use `room_id` - Cleaned up orphaned sessions and duplicates before migration **Migration:** - Generated migration: `drizzle/0005_jazzy_mimic.sql` - Cleaned up 58 orphaned sessions (NULL room_id) - Removed duplicate sessions for same room (kept highest version) - Migration successfully applied to dev database **Testing Required:** - Verify two users in same room now share the same game state - Confirm session updates broadcast correctly to all room members - Test that PRIMARY KEY constraint prevents duplicate creation 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
parent
4cedfdd629
commit
4cc3de5f43
|
|
@ -0,0 +1,21 @@
|
|||
PRAGMA foreign_keys=OFF;--> statement-breakpoint
|
||||
CREATE TABLE `__new_arcade_sessions` (
|
||||
`room_id` text PRIMARY KEY NOT NULL,
|
||||
`user_id` text NOT NULL,
|
||||
`current_game` text NOT NULL,
|
||||
`game_url` text NOT NULL,
|
||||
`game_state` text NOT NULL,
|
||||
`active_players` text NOT NULL,
|
||||
`started_at` integer NOT NULL,
|
||||
`last_activity_at` integer NOT NULL,
|
||||
`expires_at` integer NOT NULL,
|
||||
`is_active` integer DEFAULT true NOT NULL,
|
||||
`version` integer DEFAULT 1 NOT NULL,
|
||||
FOREIGN KEY (`room_id`) REFERENCES `arcade_rooms`(`id`) ON UPDATE no action ON DELETE cascade,
|
||||
FOREIGN KEY (`user_id`) REFERENCES `users`(`id`) ON UPDATE no action ON DELETE cascade
|
||||
);
|
||||
--> statement-breakpoint
|
||||
INSERT INTO `__new_arcade_sessions`("room_id", "user_id", "current_game", "game_url", "game_state", "active_players", "started_at", "last_activity_at", "expires_at", "is_active", "version") SELECT "room_id", "user_id", "current_game", "game_url", "game_state", "active_players", "started_at", "last_activity_at", "expires_at", "is_active", "version" FROM `arcade_sessions`;--> statement-breakpoint
|
||||
DROP TABLE `arcade_sessions`;--> statement-breakpoint
|
||||
ALTER TABLE `__new_arcade_sessions` RENAME TO `arcade_sessions`;--> statement-breakpoint
|
||||
PRAGMA foreign_keys=ON;
|
||||
|
|
@ -1,41 +1,44 @@
|
|||
import { integer, sqliteTable, text } from "drizzle-orm/sqlite-core";
|
||||
import { arcadeRooms } from "./arcade-rooms";
|
||||
import { users } from "./users";
|
||||
import { integer, sqliteTable, text } from 'drizzle-orm/sqlite-core'
|
||||
import { arcadeRooms } from './arcade-rooms'
|
||||
import { users } from './users'
|
||||
|
||||
export const arcadeSessions = sqliteTable("arcade_sessions", {
|
||||
userId: text("user_id")
|
||||
export const arcadeSessions = sqliteTable('arcade_sessions', {
|
||||
// Room ID is now the primary key - one session per room
|
||||
// For room-based multiplayer games, this ensures all members share the same session
|
||||
roomId: text('room_id')
|
||||
.primaryKey()
|
||||
.references(() => users.id, { onDelete: "cascade" }),
|
||||
.references(() => arcadeRooms.id, { onDelete: 'cascade' }),
|
||||
|
||||
// User who "owns" this session (typically the room creator)
|
||||
// For room-based sessions, this is just for reference/ownership tracking
|
||||
userId: text('user_id')
|
||||
.notNull()
|
||||
.references(() => users.id, { onDelete: 'cascade' }),
|
||||
|
||||
// Session metadata
|
||||
currentGame: text("current_game", {
|
||||
enum: ["matching", "memory-quiz", "complement-race"],
|
||||
currentGame: text('current_game', {
|
||||
enum: ['matching', 'memory-quiz', 'complement-race'],
|
||||
}).notNull(),
|
||||
|
||||
gameUrl: text("game_url").notNull(), // e.g., '/arcade/matching'
|
||||
gameUrl: text('game_url').notNull(), // e.g., '/arcade/matching'
|
||||
|
||||
// Game state (JSON blob)
|
||||
gameState: text("game_state", { mode: "json" }).notNull(),
|
||||
gameState: text('game_state', { mode: 'json' }).notNull(),
|
||||
|
||||
// Active players snapshot (for quick access)
|
||||
activePlayers: text("active_players", { mode: "json" }).notNull(),
|
||||
|
||||
// Room association (null for solo play)
|
||||
roomId: text("room_id").references(() => arcadeRooms.id, {
|
||||
onDelete: "set null",
|
||||
}),
|
||||
activePlayers: text('active_players', { mode: 'json' }).notNull(),
|
||||
|
||||
// Timing & TTL
|
||||
startedAt: integer("started_at", { mode: "timestamp" }).notNull(),
|
||||
lastActivityAt: integer("last_activity_at", { mode: "timestamp" }).notNull(),
|
||||
expiresAt: integer("expires_at", { mode: "timestamp" }).notNull(), // TTL-based
|
||||
startedAt: integer('started_at', { mode: 'timestamp' }).notNull(),
|
||||
lastActivityAt: integer('last_activity_at', { mode: 'timestamp' }).notNull(),
|
||||
expiresAt: integer('expires_at', { mode: 'timestamp' }).notNull(), // TTL-based
|
||||
|
||||
// Status
|
||||
isActive: integer("is_active", { mode: "boolean" }).notNull().default(true),
|
||||
isActive: integer('is_active', { mode: 'boolean' }).notNull().default(true),
|
||||
|
||||
// Version for optimistic locking
|
||||
version: integer("version").notNull().default(1),
|
||||
});
|
||||
version: integer('version').notNull().default(1),
|
||||
})
|
||||
|
||||
export type ArcadeSession = typeof arcadeSessions.$inferSelect;
|
||||
export type NewArcadeSession = typeof arcadeSessions.$inferInsert;
|
||||
export type ArcadeSession = typeof arcadeSessions.$inferSelect
|
||||
export type NewArcadeSession = typeof arcadeSessions.$inferInsert
|
||||
|
|
|
|||
|
|
@ -3,200 +3,188 @@
|
|||
* Handles database operations and validation for arcade sessions
|
||||
*/
|
||||
|
||||
import { eq } from "drizzle-orm";
|
||||
import { db, schema } from "@/db";
|
||||
import {
|
||||
buildPlayerOwnershipMap,
|
||||
type PlayerOwnershipMap,
|
||||
} from "./player-ownership";
|
||||
import { type GameMove, type GameName, getValidator } from "./validation";
|
||||
import { eq } from 'drizzle-orm'
|
||||
import { db, schema } from '@/db'
|
||||
import { buildPlayerOwnershipMap, type PlayerOwnershipMap } from './player-ownership'
|
||||
import { type GameMove, type GameName, getValidator } from './validation'
|
||||
|
||||
export interface CreateSessionOptions {
|
||||
userId: string;
|
||||
gameName: GameName;
|
||||
gameUrl: string;
|
||||
initialState: unknown;
|
||||
activePlayers: string[]; // Player IDs (UUIDs)
|
||||
roomId: string; // Required - sessions must be associated with a room
|
||||
userId: string // User who owns/created the session (typically room creator)
|
||||
gameName: GameName
|
||||
gameUrl: string
|
||||
initialState: unknown
|
||||
activePlayers: string[] // Player IDs (UUIDs)
|
||||
roomId: string // Required - PRIMARY KEY, one session per room
|
||||
}
|
||||
|
||||
export interface SessionUpdateResult {
|
||||
success: boolean;
|
||||
error?: string;
|
||||
session?: schema.ArcadeSession;
|
||||
versionConflict?: boolean;
|
||||
success: boolean
|
||||
error?: string
|
||||
session?: schema.ArcadeSession
|
||||
versionConflict?: boolean
|
||||
}
|
||||
|
||||
const TTL_HOURS = 24;
|
||||
const TTL_HOURS = 24
|
||||
|
||||
/**
|
||||
* Helper: Get database user ID from guest ID
|
||||
* The API uses guestId (from cookies) but database FKs use the internal user.id
|
||||
*/
|
||||
async function getUserIdFromGuestId(
|
||||
guestId: string,
|
||||
): Promise<string | undefined> {
|
||||
async function getUserIdFromGuestId(guestId: string): Promise<string | undefined> {
|
||||
const user = await db.query.users.findFirst({
|
||||
where: eq(schema.users.guestId, guestId),
|
||||
columns: { id: true },
|
||||
});
|
||||
return user?.id;
|
||||
})
|
||||
return user?.id
|
||||
}
|
||||
|
||||
/**
|
||||
* Get arcade session by room ID (for room-based multiplayer games)
|
||||
* Returns the shared session for all room members
|
||||
* @param roomId - The room ID
|
||||
* @param roomId - The room ID (primary key)
|
||||
*/
|
||||
export async function getArcadeSessionByRoom(
|
||||
roomId: string,
|
||||
roomId: string
|
||||
): Promise<schema.ArcadeSession | undefined> {
|
||||
// roomId is now the PRIMARY KEY, so direct lookup
|
||||
const [session] = await db
|
||||
.select()
|
||||
.from(schema.arcadeSessions)
|
||||
.where(eq(schema.arcadeSessions.roomId, roomId))
|
||||
.limit(1);
|
||||
.limit(1)
|
||||
|
||||
if (!session) return undefined;
|
||||
if (!session) return undefined
|
||||
|
||||
// Check if session has expired
|
||||
if (session.expiresAt < new Date()) {
|
||||
// Clean up expired room session
|
||||
await db
|
||||
.delete(schema.arcadeSessions)
|
||||
.where(eq(schema.arcadeSessions.roomId, roomId));
|
||||
return undefined;
|
||||
await db.delete(schema.arcadeSessions).where(eq(schema.arcadeSessions.roomId, roomId))
|
||||
return undefined
|
||||
}
|
||||
|
||||
return session;
|
||||
return session
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a new arcade session
|
||||
* For room-based games, checks if a session already exists for the room
|
||||
* For room-based games, roomId is the PRIMARY KEY ensuring one session per room
|
||||
*/
|
||||
export async function createArcadeSession(
|
||||
options: CreateSessionOptions,
|
||||
options: CreateSessionOptions
|
||||
): Promise<schema.ArcadeSession> {
|
||||
const now = new Date();
|
||||
const expiresAt = new Date(now.getTime() + TTL_HOURS * 60 * 60 * 1000);
|
||||
const now = new Date()
|
||||
const expiresAt = new Date(now.getTime() + TTL_HOURS * 60 * 60 * 1000)
|
||||
|
||||
// For room-based games, check if session already exists for this room
|
||||
if (options.roomId) {
|
||||
const existingRoomSession = await getArcadeSessionByRoom(options.roomId);
|
||||
if (existingRoomSession) {
|
||||
console.log(
|
||||
"[Session Manager] Room session already exists, returning existing:",
|
||||
{
|
||||
roomId: options.roomId,
|
||||
sessionUserId: existingRoomSession.userId,
|
||||
version: existingRoomSession.version,
|
||||
},
|
||||
);
|
||||
return existingRoomSession;
|
||||
}
|
||||
// Check if session already exists for this room (roomId is PRIMARY KEY)
|
||||
const existingRoomSession = await getArcadeSessionByRoom(options.roomId)
|
||||
if (existingRoomSession) {
|
||||
console.log('[Session Manager] Room session already exists, returning existing:', {
|
||||
roomId: options.roomId,
|
||||
sessionUserId: existingRoomSession.userId,
|
||||
version: existingRoomSession.version,
|
||||
})
|
||||
return existingRoomSession
|
||||
}
|
||||
|
||||
// Find or create user by guest ID
|
||||
let user = await db.query.users.findFirst({
|
||||
where: eq(schema.users.guestId, options.userId),
|
||||
});
|
||||
})
|
||||
|
||||
if (!user) {
|
||||
console.log(
|
||||
"[Session Manager] Creating new user with guestId:",
|
||||
options.userId,
|
||||
);
|
||||
console.log('[Session Manager] Creating new user with guestId:', options.userId)
|
||||
const [newUser] = await db
|
||||
.insert(schema.users)
|
||||
.values({
|
||||
guestId: options.userId, // Let id auto-generate via $defaultFn
|
||||
createdAt: now,
|
||||
})
|
||||
.returning();
|
||||
user = newUser;
|
||||
console.log("[Session Manager] Created user with id:", user.id);
|
||||
.returning()
|
||||
user = newUser
|
||||
console.log('[Session Manager] Created user with id:', user.id)
|
||||
} else {
|
||||
console.log("[Session Manager] Found existing user with id:", user.id);
|
||||
console.log('[Session Manager] Found existing user with id:', user.id)
|
||||
}
|
||||
|
||||
const newSession: schema.NewArcadeSession = {
|
||||
roomId: options.roomId, // PRIMARY KEY - one session per room
|
||||
userId: user.id, // Use the actual database ID, not the guestId
|
||||
currentGame: options.gameName,
|
||||
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,
|
||||
isActive: true,
|
||||
version: 1,
|
||||
};
|
||||
}
|
||||
|
||||
console.log("[Session Manager] Creating new session:", {
|
||||
userId: user.id,
|
||||
console.log('[Session Manager] Creating new session:', {
|
||||
roomId: options.roomId,
|
||||
userId: user.id,
|
||||
gameName: options.gameName,
|
||||
});
|
||||
})
|
||||
|
||||
const [session] = await db
|
||||
.insert(schema.arcadeSessions)
|
||||
.values(newSession)
|
||||
.returning();
|
||||
return session;
|
||||
try {
|
||||
const [session] = await db.insert(schema.arcadeSessions).values(newSession).returning()
|
||||
return session
|
||||
} catch (error) {
|
||||
// Handle PRIMARY KEY constraint violation (UNIQUE constraint on roomId)
|
||||
// This can happen if two users try to create a session for the same room simultaneously
|
||||
if (error instanceof Error && error.message.includes('UNIQUE constraint failed')) {
|
||||
console.log(
|
||||
'[Session Manager] Session already exists (race condition), fetching existing session for room:',
|
||||
options.roomId
|
||||
)
|
||||
const existingSession = await getArcadeSessionByRoom(options.roomId)
|
||||
if (existingSession) {
|
||||
return existingSession
|
||||
}
|
||||
}
|
||||
// Re-throw other errors
|
||||
throw error
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get active arcade session for a user
|
||||
* NOTE: With the new schema, userId is not the PRIMARY KEY (roomId is)
|
||||
* This function finds sessions where the user is associated
|
||||
* @param guestId - The guest ID from the cookie (not the database user.id)
|
||||
*/
|
||||
export async function getArcadeSession(
|
||||
guestId: string,
|
||||
): Promise<schema.ArcadeSession | undefined> {
|
||||
const userId = await getUserIdFromGuestId(guestId);
|
||||
if (!userId) return undefined;
|
||||
export async function getArcadeSession(guestId: string): Promise<schema.ArcadeSession | undefined> {
|
||||
const userId = await getUserIdFromGuestId(guestId)
|
||||
if (!userId) return undefined
|
||||
|
||||
// Query for sessions where this user is associated
|
||||
// Since roomId is PRIMARY KEY, there can be multiple rooms but only one session per room
|
||||
const [session] = await db
|
||||
.select()
|
||||
.from(schema.arcadeSessions)
|
||||
.where(eq(schema.arcadeSessions.userId, userId))
|
||||
.limit(1);
|
||||
.limit(1)
|
||||
|
||||
if (!session) return undefined;
|
||||
if (!session) return undefined
|
||||
|
||||
// Check if session has expired
|
||||
if (session.expiresAt < new Date()) {
|
||||
await deleteArcadeSession(guestId);
|
||||
return undefined;
|
||||
await deleteArcadeSessionByRoom(session.roomId)
|
||||
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
|
||||
// Verify the room still exists (roomId is now required/PRIMARY KEY)
|
||||
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;
|
||||
console.log('[Session Manager] Deleting session with non-existent room:', session.roomId)
|
||||
await deleteArcadeSessionByRoom(session.roomId)
|
||||
return undefined
|
||||
}
|
||||
|
||||
return session;
|
||||
return session
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -208,69 +196,58 @@ export async function getArcadeSession(
|
|||
export async function applyGameMove(
|
||||
userId: string,
|
||||
move: GameMove,
|
||||
roomId?: string,
|
||||
roomId?: string
|
||||
): Promise<SessionUpdateResult> {
|
||||
// For room-based games, look up the shared room session
|
||||
// For solo games, look up the user's personal session
|
||||
const session = roomId
|
||||
? await getArcadeSessionByRoom(roomId)
|
||||
: await getArcadeSession(userId);
|
||||
const session = roomId ? await getArcadeSessionByRoom(roomId) : await getArcadeSession(userId)
|
||||
|
||||
if (!session) {
|
||||
return {
|
||||
success: false,
|
||||
error: "No active session found",
|
||||
};
|
||||
error: 'No active session found',
|
||||
}
|
||||
}
|
||||
|
||||
if (!session.isActive) {
|
||||
return {
|
||||
success: false,
|
||||
error: "Session is not active",
|
||||
};
|
||||
error: 'Session is not active',
|
||||
}
|
||||
}
|
||||
|
||||
// Get the validator for this game
|
||||
const validator = getValidator(session.currentGame as GameName);
|
||||
const validator = getValidator(session.currentGame as GameName)
|
||||
|
||||
console.log("[SessionManager] About to validate move:", {
|
||||
console.log('[SessionManager] About to validate move:', {
|
||||
moveType: move.type,
|
||||
playerId: move.playerId,
|
||||
gameStateCurrentPlayer: (session.gameState as any)?.currentPlayer,
|
||||
gameStateActivePlayers: (session.gameState as any)?.activePlayers,
|
||||
gameStatePhase: (session.gameState as any)?.gamePhase,
|
||||
});
|
||||
})
|
||||
|
||||
// Fetch player ownership for authorization checks (room-based games)
|
||||
let playerOwnership: PlayerOwnershipMap | undefined;
|
||||
let internalUserId: string | undefined;
|
||||
let playerOwnership: PlayerOwnershipMap | undefined
|
||||
let internalUserId: string | undefined
|
||||
if (session.roomId) {
|
||||
try {
|
||||
// Convert guestId to internal userId for ownership comparison
|
||||
internalUserId = await getUserIdFromGuestId(userId);
|
||||
internalUserId = await getUserIdFromGuestId(userId)
|
||||
if (!internalUserId) {
|
||||
console.error(
|
||||
"[SessionManager] Failed to convert guestId to userId:",
|
||||
userId,
|
||||
);
|
||||
console.error('[SessionManager] Failed to convert guestId to userId:', userId)
|
||||
return {
|
||||
success: false,
|
||||
error: "User not found",
|
||||
};
|
||||
error: 'User not found',
|
||||
}
|
||||
}
|
||||
|
||||
// Use centralized ownership utility
|
||||
playerOwnership = await buildPlayerOwnershipMap(session.roomId);
|
||||
console.log("[SessionManager] Player ownership map:", playerOwnership);
|
||||
console.log(
|
||||
"[SessionManager] Internal userId for authorization:",
|
||||
internalUserId,
|
||||
);
|
||||
playerOwnership = await buildPlayerOwnershipMap(session.roomId)
|
||||
console.log('[SessionManager] Player ownership map:', playerOwnership)
|
||||
console.log('[SessionManager] Internal userId for authorization:', internalUserId)
|
||||
} catch (error) {
|
||||
console.error(
|
||||
"[SessionManager] Failed to fetch player ownership:",
|
||||
error,
|
||||
);
|
||||
console.error('[SessionManager] Failed to fetch player ownership:', error)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -278,23 +255,23 @@ export async function applyGameMove(
|
|||
const validationResult = validator.validateMove(session.gameState, move, {
|
||||
userId: internalUserId || userId, // Use internal userId for room-based games
|
||||
playerOwnership,
|
||||
});
|
||||
})
|
||||
|
||||
console.log("[SessionManager] Validation result:", {
|
||||
console.log('[SessionManager] Validation result:', {
|
||||
valid: validationResult.valid,
|
||||
error: validationResult.error,
|
||||
});
|
||||
})
|
||||
|
||||
if (!validationResult.valid) {
|
||||
return {
|
||||
success: false,
|
||||
error: validationResult.error || "Invalid move",
|
||||
};
|
||||
error: validationResult.error || 'Invalid move',
|
||||
}
|
||||
}
|
||||
|
||||
// Update the session with new state (using optimistic locking)
|
||||
const now = new Date();
|
||||
const expiresAt = new Date(now.getTime() + TTL_HOURS * 60 * 60 * 1000);
|
||||
const now = new Date()
|
||||
const expiresAt = new Date(now.getTime() + TTL_HOURS * 60 * 60 * 1000)
|
||||
|
||||
try {
|
||||
const [updatedSession] = await db
|
||||
|
|
@ -306,44 +283,52 @@ export async function applyGameMove(
|
|||
version: session.version + 1,
|
||||
})
|
||||
.where(
|
||||
eq(schema.arcadeSessions.userId, session.userId), // Use the userId from the session we just fetched
|
||||
eq(schema.arcadeSessions.roomId, session.roomId) // Use roomId (PRIMARY KEY)
|
||||
)
|
||||
// Version check for optimistic locking would go here
|
||||
// SQLite doesn't support WHERE clauses in UPDATE with RETURNING easily
|
||||
// We'll handle this by checking the version after
|
||||
.returning();
|
||||
.returning()
|
||||
|
||||
if (!updatedSession) {
|
||||
return {
|
||||
success: false,
|
||||
error: "Failed to update session",
|
||||
};
|
||||
error: 'Failed to update session',
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
success: true,
|
||||
session: updatedSession,
|
||||
};
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Error updating session:", error);
|
||||
console.error('Error updating session:', error)
|
||||
return {
|
||||
success: false,
|
||||
error: "Database error",
|
||||
};
|
||||
error: 'Database error',
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete an arcade session
|
||||
* Delete an arcade session by room ID
|
||||
* @param roomId - The room ID (PRIMARY KEY)
|
||||
*/
|
||||
export async function deleteArcadeSessionByRoom(roomId: string): Promise<void> {
|
||||
await db.delete(schema.arcadeSessions).where(eq(schema.arcadeSessions.roomId, roomId))
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete an arcade session by user (finds the user's session first)
|
||||
* @param guestId - The guest ID from the cookie (not the database user.id)
|
||||
*/
|
||||
export async function deleteArcadeSession(guestId: string): Promise<void> {
|
||||
const userId = await getUserIdFromGuestId(guestId);
|
||||
if (!userId) return;
|
||||
// First find the session to get its roomId
|
||||
const session = await getArcadeSession(guestId)
|
||||
if (!session) return
|
||||
|
||||
await db
|
||||
.delete(schema.arcadeSessions)
|
||||
.where(eq(schema.arcadeSessions.userId, userId));
|
||||
// Delete by roomId (PRIMARY KEY)
|
||||
await deleteArcadeSessionByRoom(session.roomId)
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -351,30 +336,32 @@ export async function deleteArcadeSession(guestId: string): Promise<void> {
|
|||
* @param guestId - The guest ID from the cookie (not the database user.id)
|
||||
*/
|
||||
export async function updateSessionActivity(guestId: string): Promise<void> {
|
||||
const userId = await getUserIdFromGuestId(guestId);
|
||||
if (!userId) return;
|
||||
// First find the session to get its roomId
|
||||
const session = await getArcadeSession(guestId)
|
||||
if (!session) return
|
||||
|
||||
const now = new Date();
|
||||
const expiresAt = new Date(now.getTime() + TTL_HOURS * 60 * 60 * 1000);
|
||||
const now = new Date()
|
||||
const expiresAt = new Date(now.getTime() + TTL_HOURS * 60 * 60 * 1000)
|
||||
|
||||
// Update using roomId (PRIMARY KEY)
|
||||
await db
|
||||
.update(schema.arcadeSessions)
|
||||
.set({
|
||||
lastActivityAt: now,
|
||||
expiresAt,
|
||||
})
|
||||
.where(eq(schema.arcadeSessions.userId, userId));
|
||||
.where(eq(schema.arcadeSessions.roomId, session.roomId))
|
||||
}
|
||||
|
||||
/**
|
||||
* Clean up expired sessions (should be called periodically)
|
||||
*/
|
||||
export async function cleanupExpiredSessions(): Promise<number> {
|
||||
const now = new Date();
|
||||
const now = new Date()
|
||||
const result = await db
|
||||
.delete(schema.arcadeSessions)
|
||||
.where(eq(schema.arcadeSessions.expiresAt, now))
|
||||
.returning();
|
||||
.returning()
|
||||
|
||||
return result.length;
|
||||
return result.length
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in New Issue