import { computed, onBeforeUnmount, onMounted, type ComputedRef } from 'vue' import type { RouteLocationNormalizedLoaded, Router } from 'vue-router' import type { RoomMetaSnapshotState } from '../../../store/state' import type { PlayerState, Tile } from '../../../types/state' import { wsClient, type WsStatus } from '../../../ws/client' import { buildWsUrl } from '../../../ws/url' import type { DisplayPlayer } from '../types' import { useChengduGameSession } from './useChengduGameSession' import { createChengduMessageHandlers } from '../socket/createMessageHandlers' interface UseChengduGameSocketOptions { route: RouteLocationNormalizedLoaded router: Router gameStore: { roomId: string phase: string players: Record dealerIndex: number currentTurn: number remainingTiles: number needDraw: boolean pendingClaim?: any scores: Record winners: string[] currentRound: number totalRounds: number resetGame: () => void } roomMeta: { value: RoomMetaSnapshotState | null } roomName: ComputedRef myHandTiles: ComputedRef myPlayer: ComputedRef session: ReturnType } export function useChengduGameSocket(options: UseChengduGameSocketOptions) { let unsubscribe: (() => void) | null = null let needsInitialRoomInfo = false const showSettlementOverlay = computed( () => options.gameStore.phase === 'settlement' && !options.session.settlementOverlayDismissed.value, ) const settlementCountdown = computed(() => { if (!showSettlementOverlay.value || !options.session.settlementDeadlineMs.value) { return null } return Math.max( 0, Math.ceil((options.session.settlementDeadlineMs.value - options.session.now.value) / 1000), ) }) const handlers = createChengduMessageHandlers({ router: options.router, gameStore: options.gameStore, roomMeta: options.roomMeta, roomName: options.roomName, myHandTiles: options.myHandTiles, myPlayer: options.myPlayer, session: options.session, }) function requestRoomInfo(): void { const routeRoomId = typeof options.route.params.roomId === 'string' ? options.route.params.roomId : '' const roomId = routeRoomId || options.gameStore.roomId || options.roomMeta.value?.roomId || '' if (!roomId || options.session.wsStatus.value !== 'connected') { return } needsInitialRoomInfo = false options.session.wsMessages.value.push(`[client] get_room_info ${roomId}`) wsClient.send({ type: 'get_room_info', roomId, payload: { room_id: roomId, }, }) } function handleSocketError(message: string): void { options.session.markDiscardCompleted() options.session.clearTurnActionPending() options.session.wsError.value = message options.session.wsMessages.value.push(`[error] ${message}`) const nowMs = Date.now() if (nowMs - options.session.lastForcedRefreshAtRef.value > 5000) { options.session.lastForcedRefreshAtRef.value = nowMs void options.session .resolveWsToken(true, true) .then((refreshedToken) => { if (!refreshedToken) { return } options.session.wsError.value = '' wsClient.reconnect(buildWsUrl(), refreshedToken) }) .catch(() => { options.session.logoutToLogin() }) } } onMounted(() => { const routeRoomId = typeof options.route.params.roomId === 'string' ? options.route.params.roomId : '' needsInitialRoomInfo = true void options.session.ensureCurrentUserLoaded().finally(() => { handlers.hydrateFromActiveRoom(routeRoomId) if (routeRoomId) { options.gameStore.roomId = routeRoomId } if (options.session.wsStatus.value === 'connected' && needsInitialRoomInfo) { requestRoomInfo() } }) const statusHandler = (status: WsStatus) => { options.session.wsStatus.value = status if (status === 'connected' && needsInitialRoomInfo) { requestRoomInfo() } } wsClient.onMessage(handlers.handleSocketMessage) wsClient.onError(handleSocketError) unsubscribe = wsClient.onStatusChange(statusHandler) void options.session.ensureWsConnected() }) onBeforeUnmount(() => { if (unsubscribe) { unsubscribe() unsubscribe = null } }) return { showSettlementOverlay, settlementCountdown, } }