fix: lazy-load map data in know-your-world validator

Fix production error where know-your-world game failed to load sessions due to ES module imports in CommonJS context.

**Problem:**
- Validator imported maps.ts at module init time
- maps.ts statically imports @svg-maps/world and @svg-maps/usa (ES modules)
- Server (CommonJS) cannot require() ES modules synchronously
- Error: "Unexpected token 'export'"

**Solution:**
- Make validateMove() async (already supported by GameValidator interface)
- Lazy-load getFilteredMapData() only when needed via dynamic import()
- Prevents ES module loading until validator method is actually called
- Client-side code continues to work normally (bundled by Next.js)
- Mark know-your-world page as force-dynamic to avoid SSR issues

**Changes:**
- GameValidator.validateMove: Now supports Promise<ValidationResult>
- KnowYourWorldValidator: Use getFilteredMapDataLazy() wrapper
- session-manager: Await validator.validateMove()
- know-your-world page: Add dynamic = 'force-dynamic' export

Fixes the "Failed to fetch session" error for know-your-world game.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
Thomas Hallock 2025-11-20 07:23:17 -06:00
parent ca8e6e46ef
commit 07c25a2296
5 changed files with 24 additions and 32 deletions

View File

@ -57,7 +57,8 @@
"Bash(pnpm remove:*)",
"Bash(gh run view:*)",
"Bash(pnpm install:*)",
"Bash(git checkout:*)"
"Bash(git checkout:*)",
"Bash(node server.js:*)"
],
"deny": [],
"ask": []

View File

@ -4,6 +4,9 @@ import { knowYourWorldGame } from '@/arcade-games/know-your-world'
const { Provider, GameComponent } = knowYourWorldGame
// Opt-out of static generation due to ES module dependencies in map data
export const dynamic = 'force-dynamic'
export default function KnowYourWorldPage() {
return (
<Provider>

View File

@ -5,19 +5,27 @@ import type {
KnowYourWorldState,
GuessRecord,
} from './types'
import { getMapData, getFilteredMapData } from './maps'
/**
* Lazy-load map functions to avoid importing ES modules at module init time
* This is critical for server-side usage where ES modules can't be required
*/
async function getFilteredMapDataLazy(...args: Parameters<typeof import('./maps').getFilteredMapData>) {
const { getFilteredMapData } = await import('./maps')
return getFilteredMapData(...args)
}
export class KnowYourWorldValidator
implements GameValidator<KnowYourWorldState, KnowYourWorldMove>
{
validateMove(state: KnowYourWorldState, move: KnowYourWorldMove): ValidationResult {
async validateMove(state: KnowYourWorldState, move: KnowYourWorldMove): Promise<ValidationResult> {
switch (move.type) {
case 'START_GAME':
return this.validateStartGame(state, move.data)
return await this.validateStartGame(state, move.data)
case 'CLICK_REGION':
return this.validateClickRegion(state, move.playerId, move.data)
case 'NEXT_ROUND':
return this.validateNextRound(state)
return await this.validateNextRound(state)
case 'END_GAME':
return this.validateEndGame(state)
case 'END_STUDY':
@ -39,42 +47,21 @@ export class KnowYourWorldValidator
}
}
private validateStartGame(state: KnowYourWorldState, data: any): ValidationResult {
private async validateStartGame(state: KnowYourWorldState, data: any): Promise<ValidationResult> {
if (state.gamePhase !== 'setup') {
return { valid: false, error: 'Can only start from setup phase' }
}
const { activePlayers, playerMetadata, selectedMap, gameMode, difficulty } = data
console.log('[KnowYourWorld Validator] Starting game with:', {
selectedMap,
gameMode,
difficulty,
studyDuration: state.studyDuration,
activePlayers: activePlayers?.length,
})
if (!activePlayers || activePlayers.length === 0) {
return { valid: false, error: 'Need at least 1 player' }
}
// Get map data and shuffle regions (with continent and difficulty filters)
const mapData = getFilteredMapData(selectedMap, state.selectedContinent, difficulty)
console.log('[KnowYourWorld Validator] Map data loaded:', {
map: mapData.id,
continent: state.selectedContinent,
difficulty,
regionsCount: mapData.regions.length,
regionIds: mapData.regions.map((r) => r.id).slice(0, 10),
regionNames: mapData.regions.map((r) => r.name).slice(0, 10),
})
const mapData = await getFilteredMapDataLazy(selectedMap, state.selectedContinent, difficulty)
const regionIds = mapData.regions.map((r) => r.id)
const shuffledRegions = this.shuffleArray([...regionIds])
console.log('[KnowYourWorld Validator] First region to find:', {
id: shuffledRegions[0],
name: mapData.regions.find((r) => r.id === shuffledRegions[0])?.name,
totalShuffled: shuffledRegions.length,
})
// Initialize scores and attempts
const scores: Record<string, number> = {}
@ -225,13 +212,13 @@ export class KnowYourWorldValidator
}
}
private validateNextRound(state: KnowYourWorldState): ValidationResult {
private async validateNextRound(state: KnowYourWorldState): Promise<ValidationResult> {
if (state.gamePhase !== 'results') {
return { valid: false, error: 'Can only start next round from results' }
}
// Get map data and shuffle regions (with continent and difficulty filters)
const mapData = getFilteredMapData(state.selectedMap, state.selectedContinent, state.difficulty)
const mapData = await getFilteredMapDataLazy(state.selectedMap, state.selectedContinent, state.difficulty)
const regionIds = mapData.regions.map((r) => r.id)
const shuffledRegions = this.shuffleArray([...regionIds])

View File

@ -227,7 +227,7 @@ export async function applyGameMove(
}
// Validate the move with authorization context (use internal userId, not guestId)
const validationResult = validator.validateMove(session.gameState, move, {
const validationResult = await validator.validateMove(session.gameState, move, {
userId: internalUserId || userId, // Use internal userId for room-based games
playerOwnership,
})

View File

@ -68,11 +68,12 @@ export interface ValidationContext {
export interface GameValidator<TState = unknown, TMove extends GameMove = GameMove> {
/**
* Validate a game move and return the new state if valid
* Can be async to support lazy-loaded dependencies (e.g., ES modules)
* @param state Current game state
* @param move The move to validate
* @param context Optional validation context for authorization checks
*/
validateMove(state: TState, move: TMove, context?: ValidationContext): ValidationResult
validateMove(state: TState, move: TMove, context?: ValidationContext): ValidationResult | Promise<ValidationResult>
/**
* Check if the game is in a terminal state (completed)