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";
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<number | null>(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();
};
if (connection) {
bindConnectionEvents(connection);
} else {
// 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 → 无线循环)

View File

@@ -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,
});
}