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:
@@ -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 → 无线循环)
|
||||||
|
|||||||
@@ -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,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user