fix: improve Reverb WebSocket connect and reconnect handling

Avoid premature polling fallback while Pusher is connecting, schedule
reconnect after disconnect, and disable Pusher stats for Reverb.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
2026-05-30 09:52:42 +08:00
parent 4fc2b38a40
commit 9f43d07778
2 changed files with 63 additions and 16 deletions

View File

@@ -1,10 +1,10 @@
"use client"; "use client";
import { useCallback, useEffect, useRef } from "react"; import { useCallback, useEffect, useRef, useState } from "react";
import { getWalletBalance } from "@/api/wallet"; import { getWalletBalance } from "@/api/wallet";
import { getActivePlayerCurrencyFromStore } from "@/lib/player-currency"; import { getActivePlayerCurrencyFromStore } from "@/lib/player-currency";
import { getLotteryEcho } from "@/lib/lottery-echo"; import { disconnectLotteryEcho, getLotteryEcho } from "@/lib/lottery-echo";
import { import {
useNetworkConnectionStore, useNetworkConnectionStore,
type NetworkMode, type NetworkMode,
@@ -15,6 +15,19 @@ const WALLET_POLLING_DURATION_MS = 2 * 60 * 1000; // 2分钟限时轮询
const RECONNECT_INTERVAL_MS = 5_000; // 5秒重连间隔 const RECONNECT_INTERVAL_MS = 5_000; // 5秒重连间隔
const MAX_RECONNECT_ATTEMPTS = 10; // 最大重连次数 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 = { export type UseWebSocketManagerReturn = {
/** 当前网络模式 */ /** 当前网络模式 */
mode: NetworkMode; mode: NetworkMode;
@@ -45,6 +58,8 @@ export function useWebSocketManager(): UseWebSocketManagerReturn {
const store = useNetworkConnectionStore(); const store = useNetworkConnectionStore();
const reconnectTimerRef = useRef<number | null>(null); const reconnectTimerRef = useRef<number | null>(null);
const attemptReconnectRef = useRef<() => void>(() => {}); const attemptReconnectRef = useRef<() => void>(() => {});
/** 递增后重新创建 Echo 并绑定连接事件(供「恢复」与重连使用) */
const [echoSession, setEchoSession] = useState(0);
const { const {
mode, mode,
@@ -135,6 +150,7 @@ export function useWebSocketManager(): UseWebSocketManagerReturn {
} }
setReconnecting(true); setReconnecting(true);
attemptReconnectRef.current();
}, [ }, [
setWebSocketConnected, setWebSocketConnected,
setLastDisconnectedAt, setLastDisconnectedAt,
@@ -192,12 +208,17 @@ export function useWebSocketManager(): UseWebSocketManagerReturn {
attemptReconnectRef.current = attemptReconnect; attemptReconnectRef.current = attemptReconnect;
}, [attemptReconnect]); }, [attemptReconnect]);
// 手动重连 // 手动重连:断开旧 Echo、重建连接仅 reset 计数无法修复 failed 状态)
const reconnect = useCallback(() => { const reconnect = useCallback(() => {
disconnectLotteryEcho();
resetReconnectAttempts(); resetReconnectAttempts();
setReconnecting(true); setReconnecting(true);
attemptReconnect(); if (reconnectTimerRef.current !== null) {
}, [resetReconnectAttempts, setReconnecting, attemptReconnect]); window.clearTimeout(reconnectTimerRef.current);
reconnectTimerRef.current = null;
}
setEchoSession((n) => n + 1);
}, [resetReconnectAttempts, setReconnecting]);
// 监听网络状态变化 // 监听网络状态变化
useEffect(() => { useEffect(() => {
@@ -232,13 +253,17 @@ export function useWebSocketManager(): UseWebSocketManagerReturn {
echo.channel("lottery-hall"); echo.channel("lottery-hall");
const pusher = echo.connector?.pusher; let connection = echo.connector?.pusher?.connection as
const connection = pusher?.connection; | PusherConnection
| undefined;
let pendingTimer: number | null = null;
const syncConnectionState = () => { const syncConnectionState = () => {
const state = connection?.state; const state = connection?.state;
if (state === "connected") { if (state === "connected") {
handleConnect(); handleConnect();
} else if (state && PUSHER_PENDING_STATES.has(state)) {
setReconnecting(true);
} else if ( } else if (
state === "disconnected" || state === "disconnected" ||
state === "unavailable" || state === "unavailable" ||
@@ -248,34 +273,55 @@ export function useWebSocketManager(): UseWebSocketManagerReturn {
} }
}; };
if (connection) { const bindConnectionEvents = (conn: PusherConnection) => {
connection.bind("connected", handleConnect); conn.bind("connected", handleConnect);
connection.bind("disconnected", handleDisconnect); conn.bind("disconnected", handleDisconnect);
connection.bind("unavailable", handleDisconnect); conn.bind("unavailable", handleDisconnect);
connection.bind("failed", handleDisconnect); conn.bind("failed", handleDisconnect);
conn.bind("state_change", syncConnectionState);
syncConnectionState(); syncConnectionState();
} else if (isPusherConnected()) { };
handleConnect();
if (connection) {
bindConnectionEvents(connection);
} else { } 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 () => { return () => {
if (pendingTimer !== null) {
window.clearTimeout(pendingTimer);
}
if (connection) { if (connection) {
connection.unbind("connected", handleConnect); connection.unbind("connected", handleConnect);
connection.unbind("disconnected", handleDisconnect); connection.unbind("disconnected", handleDisconnect);
connection.unbind("unavailable", handleDisconnect); connection.unbind("unavailable", handleDisconnect);
connection.unbind("failed", handleDisconnect); connection.unbind("failed", handleDisconnect);
connection.unbind("state_change", syncConnectionState);
} }
if (reconnectTimerRef.current) { if (reconnectTimerRef.current) {
window.clearTimeout(reconnectTimerRef.current); window.clearTimeout(reconnectTimerRef.current);
} }
}; };
}, [ }, [
echoSession,
isPusherConnected, isPusherConnected,
handleConnect, handleConnect,
handleDisconnect, handleDisconnect,
switchToPollingMode, switchToPollingMode,
setReconnecting,
]); ]);
// 仅挂载卸载时清理;勿依赖 stop*(否则每次 setInterval id 变化都会触发清理 → setState → 无线循环) // 仅挂载卸载时清理;勿依赖 stop*(否则每次 setInterval id 变化都会触发清理 → setState → 无线循环)

View File

@@ -43,7 +43,8 @@ export function getLotteryEcho(): Echo<"reverb"> | null {
wsPort: port, wsPort: port,
wssPort: port, wssPort: port,
forceTLS, forceTLS,
enabledTransports: forceTLS ? ["wss"] : ["ws"], enabledTransports: forceTLS ? ["ws", "wss"] : ["ws"],
disableStats: true,
}); });
} }