From 57631c2bdc8ed0a6fc7fb7b9604b3d694d984938 Mon Sep 17 00:00:00 2001 From: Andy Aylward Date: Sun, 8 Mar 2026 16:22:35 -0400 Subject: [PATCH] feat(golf): add JWT session token support for WebSocket reconnection Send authenticate message with stored JWT on connect so the server can restore player identity after disconnections. Preserve game state during reconnect instead of resetting it. - Send authenticate with stored session token on every connect - Store session token in localStorage on authenticated response - Clear token on auth-related errors - Increase reconnect attempts from 3 to 10 (covers 5-min grace period) - Don't reset game state on disconnect (server restores on reconnect) --- src/plugins/golfNetworkPlugin.ts | 104 +++++++++++++++++++++++++------ src/utils/networkAdapter.ts | 4 +- 2 files changed, 88 insertions(+), 20 deletions(-) diff --git a/src/plugins/golfNetworkPlugin.ts b/src/plugins/golfNetworkPlugin.ts index 0a243a9..4219e1f 100644 --- a/src/plugins/golfNetworkPlugin.ts +++ b/src/plugins/golfNetworkPlugin.ts @@ -9,14 +9,15 @@ import type { Player, GameState as GolfGameState, Room, FinalScore } from '@/typ // Golf-specific message types interface GolfMessage extends BaseNetworkMessage { - type: 'createRoom' | 'joinRoom' | 'createGame' | 'joinGame' | 'roomJoined' | 'roomStateUpdate' | 'gameState' | 'error' | + type: 'authenticate' | 'authenticated' | 'createRoom' | 'joinRoom' | 'createGame' | 'joinGame' | 'roomJoined' | 'roomStateUpdate' | 'gameState' | 'error' | 'gameStarted' | 'turnChanged' | 'playerKnocked' | 'gameEnded' | 'newGameStarted' | - 'startGame' | 'peekCard' | 'drawCard' | 'takeFromDiscard' | + 'startGame' | 'peekCard' | 'drawCard' | 'takeFromDiscard' | 'swapCard' | 'discardDrawn' | 'knock' | 'hideCards' | 'startNewGame' // Request fields roomId?: string gameId?: string cardIndex?: number + sessionToken?: string // Response fields playerId?: string gameState?: GolfGameState @@ -25,6 +26,7 @@ interface GolfMessage extends BaseNetworkMessage { winner?: string finalScores?: FinalScore[] previousGameId?: string + reconnected?: boolean } // Golf plugin state @@ -36,6 +38,8 @@ interface GolfPluginState { isInLobby: boolean } +const SESSION_TOKEN_KEY = 'golf_session_token' + export class GolfNetworkPlugin implements GameNetworkPlugin { gameType = 'golf' private onRoomJoined?: (playerId: string, roomState: Room) => void @@ -68,6 +72,7 @@ export class GolfNetworkPlugin implements GameNetworkPlugin { getMessageHandlers(): MessageHandlerMap { return { + 'authenticated': (msg, ctx) => this.handleAuthenticated(msg as GolfMessage, ctx), 'roomJoined': (msg, ctx) => this.handleRoomJoined(msg as GolfMessage, ctx), 'roomStateUpdate': (msg, ctx) => this.handleRoomStateUpdate(msg as GolfMessage, ctx), 'gameJoined': (msg, ctx) => this.handleGameJoined(msg as GolfMessage, ctx), @@ -83,7 +88,7 @@ export class GolfNetworkPlugin implements GameNetworkPlugin { validateMessage(message: BaseNetworkMessage): boolean { const validTypes = [ - 'roomJoined', 'roomStateUpdate', 'gameJoined', 'gameState', 'error', + 'authenticated', 'roomJoined', 'roomStateUpdate', 'gameJoined', 'gameState', 'error', 'gameStarted', 'turnChanged', 'playerKnocked', 'gameEnded', 'newGameStarted' ] return validTypes.includes(message.type) @@ -99,21 +104,23 @@ export class GolfNetworkPlugin implements GameNetworkPlugin { } } - onConnect(_context: NetworkContext): void { + onConnect(context: NetworkContext): void { console.log('🎮 Golf game connected') + + // Send authenticate message as first message + const storedToken = this.getStoredSessionToken() + context.send({ + type: 'authenticate', + sessionToken: storedToken || '', + timestamp: Date.now() + }) + console.log(`📤 Sent authenticate request ${storedToken ? 'with stored token' : 'for new session'}`) } - onDisconnect(context: NetworkContext): void { - console.log('🎮 Golf game disconnected') - // Reset state on disconnect - context.updateGameState(s => ({ - ...s, - playerId: null, - gameState: null, - roomState: null, - gameContext: null, - isInLobby: true - })) + onDisconnect(_context: NetworkContext): void { + console.log('🎮 Golf game disconnected — keeping state for reconnection') + // Don't reset state on disconnect. The server will restore room/game + // state when we reconnect and re-authenticate with our session token. } // Message handlers @@ -196,6 +203,16 @@ export class GolfNetworkPlugin implements GameNetworkPlugin { private handleError(message: GolfMessage, context: NetworkContext): void { const errorMessage = message.message || 'Unknown error occurred' console.error('🚫 Game error:', errorMessage) + + // Clear stored session token if error is related to authentication + if (errorMessage.toLowerCase().includes('session') || + errorMessage.toLowerCase().includes('token') || + errorMessage.toLowerCase().includes('authentication') || + errorMessage.toLowerCase().includes('unauthenticated')) { + console.log('🔐 Clearing invalid session token') + this.clearSessionToken() + } + this.notify(errorMessage, context) } @@ -235,15 +252,32 @@ export class GolfNetworkPlugin implements GameNetworkPlugin { private handleNewGameStarted(message: GolfMessage, context: NetworkContext): void { const gameId = message.gameId const previousGameId = message.previousGameId - + console.log(`🆕 New game started in room! Game ID: ${gameId}${previousGameId ? `, Previous: ${previousGameId}` : ''}`) this.notify('New game started!', context) - + if (this.onNewGameStarted && gameId) { this.onNewGameStarted(gameId, previousGameId) } } + private handleAuthenticated(message: GolfMessage, _context: NetworkContext): void { + if (!message.sessionToken) { + console.error('Authenticated message missing session token') + return + } + + // Store the session token + this.storeSessionToken(message.sessionToken) + + const isReconnect = message.reconnected || false + console.log(`🔐 Authenticated successfully ${isReconnect ? '(reconnected to existing session)' : '(new session)'}`) + + if (isReconnect) { + console.log('♻️ Session restored - you should be back in your previous room/game') + } + } + // Helper method to send notifications private notify(message: string, _context: NetworkContext): void { if (this.onNotification) { @@ -362,11 +396,43 @@ export class GolfNetworkPlugin implements GameNetworkPlugin { console.log('📤 Sent start new game request') } + // Session token management + private storeSessionToken(token: string): void { + try { + localStorage.setItem(SESSION_TOKEN_KEY, token) + console.log('💾 Session token stored in localStorage') + } catch (error) { + console.error('Failed to store session token:', error) + } + } + + private getStoredSessionToken(): string | null { + try { + const token = localStorage.getItem(SESSION_TOKEN_KEY) + if (token) { + console.log('🔑 Retrieved stored session token') + } + return token + } catch (error) { + console.error('Failed to retrieve session token:', error) + return null + } + } + + private clearSessionToken(): void { + try { + localStorage.removeItem(SESSION_TOKEN_KEY) + console.log('🗑️ Session token cleared from localStorage') + } catch (error) { + console.error('Failed to clear session token:', error) + } + } + // Helper to check if it's the player's turn isMyTurn(context: NetworkContext): boolean { const state = context.getGameState() if (!state.gameState || !state.playerId) return false - + const currentPlayer = state.gameState.players[state.gameState.currentPlayerIndex] return currentPlayer?.id === state.playerId } @@ -375,7 +441,7 @@ export class GolfNetworkPlugin implements GameNetworkPlugin { getCurrentPlayer(context: NetworkContext): Player | null { const state = context.getGameState() if (!state.gameState || !state.playerId) return null - + return state.gameState.players.find(p => p.id === state.playerId) || null } } \ No newline at end of file diff --git a/src/utils/networkAdapter.ts b/src/utils/networkAdapter.ts index 012cf05..18b741c 100644 --- a/src/utils/networkAdapter.ts +++ b/src/utils/networkAdapter.ts @@ -158,7 +158,9 @@ export class GolfNetworkAdapter { this.manager.connect({ url, gameType: 'golf', - reconnect: false // Golf game doesn't auto-reconnect + reconnect: true, // Enable reconnection with JWT session restore + reconnectDelay: 2000, // Try to reconnect after 2 seconds + maxReconnectAttempts: 10 // Try up to 10 times (covers the 5-minute grace period) }) }