diff --git a/src/hooks/use-websocket-manager.ts b/src/hooks/use-websocket-manager.ts index 63fb35e..1c0eb23 100644 --- a/src/hooks/use-websocket-manager.ts +++ b/src/hooks/use-websocket-manager.ts @@ -1,10 +1,10 @@ "use client"; -import { useCallback, useEffect, useRef } from "react"; +import { useCallback, useEffect, useRef, useState } from "react"; import { getWalletBalance } from "@/api/wallet"; import { getActivePlayerCurrencyFromStore } from "@/lib/player-currency"; -import { getLotteryEcho } from "@/lib/lottery-echo"; +import { disconnectLotteryEcho, getLotteryEcho } from "@/lib/lottery-echo"; import { useNetworkConnectionStore, type NetworkMode, @@ -15,6 +15,19 @@ const WALLET_POLLING_DURATION_MS = 2 * 60 * 1000; // 2分钟限时轮询 const RECONNECT_INTERVAL_MS = 5_000; // 5秒重连间隔 const MAX_RECONNECT_ATTEMPTS = 10; // 最大重连次数 +/** Pusher 尚未连上前的中间态,不应立刻切降级 */ +const PUSHER_PENDING_STATES = new Set([ + "initialized", + "connecting", + "reconnecting", +]); + +type PusherConnection = { + state?: string; + bind: (event: string, callback: () => void) => void; + unbind: (event: string, callback: () => void) => void; +}; + export type UseWebSocketManagerReturn = { /** 当前网络模式 */ mode: NetworkMode; @@ -45,6 +58,8 @@ export function useWebSocketManager(): UseWebSocketManagerReturn { const store = useNetworkConnectionStore(); const reconnectTimerRef = useRef(null); const attemptReconnectRef = useRef<() => void>(() => {}); + /** 递增后重新创建 Echo 并绑定连接事件(供「恢复」与重连使用) */ + const [echoSession, setEchoSession] = useState(0); const { mode, @@ -135,6 +150,7 @@ export function useWebSocketManager(): UseWebSocketManagerReturn { } setReconnecting(true); + attemptReconnectRef.current(); }, [ setWebSocketConnected, setLastDisconnectedAt, @@ -192,12 +208,17 @@ export function useWebSocketManager(): UseWebSocketManagerReturn { attemptReconnectRef.current = attemptReconnect; }, [attemptReconnect]); - // 手动重连 + // 手动重连:断开旧 Echo、重建连接(仅 reset 计数无法修复 failed 状态) const reconnect = useCallback(() => { + disconnectLotteryEcho(); resetReconnectAttempts(); setReconnecting(true); - attemptReconnect(); - }, [resetReconnectAttempts, setReconnecting, attemptReconnect]); + if (reconnectTimerRef.current !== null) { + window.clearTimeout(reconnectTimerRef.current); + reconnectTimerRef.current = null; + } + setEchoSession((n) => n + 1); + }, [resetReconnectAttempts, setReconnecting]); // 监听网络状态变化 useEffect(() => { @@ -232,13 +253,17 @@ export function useWebSocketManager(): UseWebSocketManagerReturn { echo.channel("lottery-hall"); - const pusher = echo.connector?.pusher; - const connection = pusher?.connection; + let connection = echo.connector?.pusher?.connection as + | PusherConnection + | undefined; + let pendingTimer: number | null = null; const syncConnectionState = () => { const state = connection?.state; if (state === "connected") { handleConnect(); + } else if (state && PUSHER_PENDING_STATES.has(state)) { + setReconnecting(true); } else if ( state === "disconnected" || state === "unavailable" || @@ -248,34 +273,55 @@ export function useWebSocketManager(): UseWebSocketManagerReturn { } }; - if (connection) { - connection.bind("connected", handleConnect); - connection.bind("disconnected", handleDisconnect); - connection.bind("unavailable", handleDisconnect); - connection.bind("failed", handleDisconnect); + const bindConnectionEvents = (conn: PusherConnection) => { + conn.bind("connected", handleConnect); + conn.bind("disconnected", handleDisconnect); + conn.bind("unavailable", handleDisconnect); + conn.bind("failed", handleDisconnect); + conn.bind("state_change", syncConnectionState); syncConnectionState(); - } else if (isPusherConnected()) { - handleConnect(); + }; + + if (connection) { + bindConnectionEvents(connection); } else { - handleDisconnect(); + // connector 可能下一 tick 才就绪;勿立刻切降级 + pendingTimer = window.setTimeout(() => { + connection = echo.connector?.pusher?.connection as + | PusherConnection + | undefined; + if (connection) { + bindConnectionEvents(connection); + } else if (isPusherConnected()) { + handleConnect(); + } else { + handleDisconnect(); + } + }, 0); } return () => { + if (pendingTimer !== null) { + window.clearTimeout(pendingTimer); + } if (connection) { connection.unbind("connected", handleConnect); connection.unbind("disconnected", handleDisconnect); connection.unbind("unavailable", handleDisconnect); connection.unbind("failed", handleDisconnect); + connection.unbind("state_change", syncConnectionState); } if (reconnectTimerRef.current) { window.clearTimeout(reconnectTimerRef.current); } }; }, [ + echoSession, isPusherConnected, handleConnect, handleDisconnect, switchToPollingMode, + setReconnecting, ]); // 仅挂载卸载时清理;勿依赖 stop*(否则每次 setInterval id 变化都会触发清理 → setState → 无线循环) diff --git a/src/lib/lottery-echo.ts b/src/lib/lottery-echo.ts index bdfe29e..2a887da 100644 --- a/src/lib/lottery-echo.ts +++ b/src/lib/lottery-echo.ts @@ -43,7 +43,8 @@ export function getLotteryEcho(): Echo<"reverb"> | null { wsPort: port, wssPort: port, forceTLS, - enabledTransports: forceTLS ? ["wss"] : ["ws"], + enabledTransports: forceTLS ? ["ws", "wss"] : ["ws"], + disableStats: true, }); }