refactor: 引入 SWR 统一钱包余额查询与轮询逻辑
This commit is contained in:
22
package-lock.json
generated
22
package-lock.json
generated
@@ -27,6 +27,7 @@
|
||||
"react-dom": "19.2.4",
|
||||
"react-i18next": "^17.0.7",
|
||||
"sonner": "^2.0.7",
|
||||
"swr": "^2.4.1",
|
||||
"tailwind-merge": "^3.5.0",
|
||||
"tw-animate-css": "^1.4.0",
|
||||
"zustand": "^5.0.13"
|
||||
@@ -4382,6 +4383,14 @@
|
||||
"node": ">= 0.8"
|
||||
}
|
||||
},
|
||||
"node_modules/dequal": {
|
||||
"version": "2.0.3",
|
||||
"resolved": "https://mirrors.cloud.tencent.com/npm/dequal/-/dequal-2.0.3.tgz",
|
||||
"integrity": "sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA==",
|
||||
"engines": {
|
||||
"node": ">=6"
|
||||
}
|
||||
},
|
||||
"node_modules/detect-libc": {
|
||||
"version": "2.1.2",
|
||||
"resolved": "https://registry.npmmirror.com/detect-libc/-/detect-libc-2.1.2.tgz",
|
||||
@@ -9723,6 +9732,19 @@
|
||||
"url": "https://github.com/sponsors/ljharb"
|
||||
}
|
||||
},
|
||||
"node_modules/swr": {
|
||||
"version": "2.4.1",
|
||||
"resolved": "https://mirrors.cloud.tencent.com/npm/swr/-/swr-2.4.1.tgz",
|
||||
"integrity": "sha512-2CC6CiKQtEwaEeNiqWTAw9PGykW8SR5zZX8MZk6TeAvEAnVS7Visz8WzphqgtQ8v2xz/4Q5K+j+SeMaKXeeQIA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"dequal": "^2.0.3",
|
||||
"use-sync-external-store": "^1.6.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"react": "^16.11.0 || ^17.0.0 || ^18.0.0 || ^19.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/tagged-tag": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmmirror.com/tagged-tag/-/tagged-tag-1.0.0.tgz",
|
||||
|
||||
@@ -28,6 +28,7 @@
|
||||
"react-dom": "19.2.4",
|
||||
"react-i18next": "^17.0.7",
|
||||
"sonner": "^2.0.7",
|
||||
"swr": "^2.4.1",
|
||||
"tailwind-merge": "^3.5.0",
|
||||
"tw-animate-css": "^1.4.0",
|
||||
"zustand": "^5.0.13"
|
||||
|
||||
@@ -2,8 +2,9 @@
|
||||
|
||||
import { Wallet } from "lucide-react";
|
||||
import Image from "next/image";
|
||||
import { useCallback, useEffect, useRef, useState } from "react";
|
||||
import { useEffect, useRef } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { useSWRConfig } from "swr";
|
||||
|
||||
import { getWalletBalance } from "@/api/wallet";
|
||||
import { Skeleton } from "@/components/ui/skeleton";
|
||||
@@ -12,76 +13,51 @@ import {
|
||||
TransferOutDialog,
|
||||
} from "@/features/wallet/wallet-transfer-dialogs";
|
||||
import { useActivePlayerCurrency } from "@/hooks/use-active-player-currency";
|
||||
import { useApiQuery } from "@/hooks/use-api-query";
|
||||
import { formatMinorAsCurrency } from "@/lib/money";
|
||||
import { PLAYER_CURRENCY_CHANGE_EVENT } from "@/lib/player-currency-preference";
|
||||
import { cn } from "@/lib/utils";
|
||||
import { useNetworkConnectionStore } from "@/stores/network-connection-store";
|
||||
import type { WalletBalanceData } from "@/types/api/wallet-balance";
|
||||
|
||||
const BALANCE_KEY = (currency: string) => ["wallet/balance", currency];
|
||||
|
||||
export function HallWalletStrip() {
|
||||
const mode = useNetworkConnectionStore((s) => s.mode);
|
||||
const { t } = useTranslation("player");
|
||||
const { activeCurrency } = useActivePlayerCurrency();
|
||||
const [balance, setBalance] = useState<WalletBalanceData | null>(null);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const { mutate } = useSWRConfig();
|
||||
const degradedWalletPollRef = useRef<number | null>(null);
|
||||
|
||||
const currency = activeCurrency;
|
||||
const isDegraded = mode === "polling" || mode === "offline";
|
||||
|
||||
const refresh = useCallback(async () => {
|
||||
const b = await getWalletBalance({ currency: activeCurrency });
|
||||
setBalance(b);
|
||||
}, [activeCurrency]);
|
||||
const { data: balance, isLoading: loading } = useApiQuery(
|
||||
BALANCE_KEY(currency),
|
||||
() => getWalletBalance({ currency }),
|
||||
{
|
||||
// 降级模式下 60 秒自动刷新,SWR 内置定时器代替手动 setInterval
|
||||
refreshInterval: isDegraded ? 60_000 : undefined,
|
||||
},
|
||||
);
|
||||
|
||||
// 全局事件触发刷新(下注后、币种切换等场景)
|
||||
useEffect(() => {
|
||||
let cancelled = false;
|
||||
void (async () => {
|
||||
setLoading(true);
|
||||
try {
|
||||
await refresh();
|
||||
} finally {
|
||||
if (!cancelled) setLoading(false);
|
||||
}
|
||||
})();
|
||||
return () => {
|
||||
cancelled = true;
|
||||
};
|
||||
}, [refresh]);
|
||||
|
||||
useEffect(() => {
|
||||
const onRefresh = () => void refresh();
|
||||
const onRefresh = () => void mutate(BALANCE_KEY(currency));
|
||||
window.addEventListener("lottery-wallet-refresh", onRefresh);
|
||||
window.addEventListener(PLAYER_CURRENCY_CHANGE_EVENT, onRefresh);
|
||||
return () => {
|
||||
window.removeEventListener("lottery-wallet-refresh", onRefresh);
|
||||
window.removeEventListener(PLAYER_CURRENCY_CHANGE_EVENT, onRefresh);
|
||||
};
|
||||
}, [refresh]);
|
||||
}, [mutate, currency]);
|
||||
|
||||
// 非降级模式时清理遗留计时器(refreshInterval 已由 SWR 管理)
|
||||
useEffect(() => {
|
||||
if (mode !== "polling" && mode !== "offline") {
|
||||
if (degradedWalletPollRef.current !== null) {
|
||||
window.clearInterval(degradedWalletPollRef.current);
|
||||
degradedWalletPollRef.current = null;
|
||||
}
|
||||
return;
|
||||
if (!isDegraded && degradedWalletPollRef.current !== null) {
|
||||
window.clearInterval(degradedWalletPollRef.current);
|
||||
degradedWalletPollRef.current = null;
|
||||
}
|
||||
|
||||
if (degradedWalletPollRef.current !== null) {
|
||||
return;
|
||||
}
|
||||
|
||||
degradedWalletPollRef.current = window.setInterval(() => {
|
||||
void refresh();
|
||||
}, 60_000);
|
||||
|
||||
return () => {
|
||||
if (degradedWalletPollRef.current !== null) {
|
||||
window.clearInterval(degradedWalletPollRef.current);
|
||||
degradedWalletPollRef.current = null;
|
||||
}
|
||||
};
|
||||
}, [mode, refresh]);
|
||||
}, [isDegraded]);
|
||||
|
||||
const availableMinor = Number(balance?.available_balance ?? balance?.balance ?? 0);
|
||||
const isCreditPlayer =
|
||||
@@ -136,7 +112,7 @@ export function HallWalletStrip() {
|
||||
currency={currency}
|
||||
lotteryMinor={availableMinor}
|
||||
mainMinor={mainMinor}
|
||||
onSuccess={refresh}
|
||||
onSuccess={async () => { await mutate(BALANCE_KEY(currency)); }}
|
||||
/>
|
||||
<TransferOutDialog
|
||||
idPrefix="hall-"
|
||||
@@ -145,7 +121,7 @@ export function HallWalletStrip() {
|
||||
triggerClassName="h-12 rounded-lg text-base font-bold"
|
||||
currency={currency}
|
||||
availableMinor={availableMinor}
|
||||
onSuccess={refresh}
|
||||
onSuccess={async () => { await mutate(BALANCE_KEY(currency)); }}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
|
||||
@@ -1,51 +1,40 @@
|
||||
"use client";
|
||||
|
||||
import { useRouter } from "next/navigation";
|
||||
import { useCallback, useEffect, useState } from "react";
|
||||
import { useCallback, useEffect } from "react";
|
||||
import { useSWRConfig } from "swr";
|
||||
|
||||
import { getWalletBalance } from "@/api/wallet";
|
||||
import { TransferInPage } from "@/features/wallet/wallet-transfer-forms";
|
||||
import { Skeleton } from "@/components/ui/skeleton";
|
||||
import { useActivePlayerCurrency } from "@/hooks/use-active-player-currency";
|
||||
import { useApiQuery } from "@/hooks/use-api-query";
|
||||
import { PLAYER_CURRENCY_CHANGE_EVENT } from "@/lib/player-currency-preference";
|
||||
import type { WalletBalanceData } from "@/types/api/wallet-balance";
|
||||
|
||||
const BALANCE_KEY = (currency: string) => ["wallet/balance", currency];
|
||||
|
||||
/** 独立路由 `/wallet/transfer-in` */
|
||||
export function TransferInScreen() {
|
||||
const router = useRouter();
|
||||
const { activeCurrency: currency } = useActivePlayerCurrency();
|
||||
const [balance, setBalance] = useState<WalletBalanceData | null>(null);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const { mutate } = useSWRConfig();
|
||||
|
||||
const load = useCallback(async () => {
|
||||
const b = await getWalletBalance({ currency });
|
||||
setBalance(b);
|
||||
}, [currency]);
|
||||
const { data: balance, isLoading: loading } = useApiQuery(
|
||||
BALANCE_KEY(currency),
|
||||
() => getWalletBalance({ currency }),
|
||||
);
|
||||
|
||||
// 币种切换时 SWR key 自动变化,此处仅处理全局事件触发的刷新
|
||||
useEffect(() => {
|
||||
let c = false;
|
||||
void (async () => {
|
||||
try {
|
||||
await load();
|
||||
} finally {
|
||||
if (!c) setLoading(false);
|
||||
}
|
||||
})();
|
||||
return () => {
|
||||
c = true;
|
||||
};
|
||||
}, [load]);
|
||||
|
||||
useEffect(() => {
|
||||
const onCurrencyChange = () => void load();
|
||||
window.addEventListener(PLAYER_CURRENCY_CHANGE_EVENT, onCurrencyChange);
|
||||
return () => window.removeEventListener(PLAYER_CURRENCY_CHANGE_EVENT, onCurrencyChange);
|
||||
}, [load]);
|
||||
const onRefresh = () => void mutate(BALANCE_KEY(currency));
|
||||
window.addEventListener(PLAYER_CURRENCY_CHANGE_EVENT, onRefresh);
|
||||
return () => window.removeEventListener(PLAYER_CURRENCY_CHANGE_EVENT, onRefresh);
|
||||
}, [mutate, currency]);
|
||||
|
||||
const onSuccess = useCallback(async () => {
|
||||
await load();
|
||||
await mutate(BALANCE_KEY(currency));
|
||||
router.push("/wallet");
|
||||
}, [load, router]);
|
||||
}, [mutate, currency, router]);
|
||||
|
||||
if (loading && !balance) {
|
||||
return (
|
||||
|
||||
@@ -1,51 +1,40 @@
|
||||
"use client";
|
||||
|
||||
import { useRouter } from "next/navigation";
|
||||
import { useCallback, useEffect, useState } from "react";
|
||||
import { useCallback, useEffect } from "react";
|
||||
import { useSWRConfig } from "swr";
|
||||
|
||||
import { getWalletBalance } from "@/api/wallet";
|
||||
import { TransferOutPage } from "@/features/wallet/wallet-transfer-forms";
|
||||
import { Skeleton } from "@/components/ui/skeleton";
|
||||
import { useActivePlayerCurrency } from "@/hooks/use-active-player-currency";
|
||||
import { useApiQuery } from "@/hooks/use-api-query";
|
||||
import { PLAYER_CURRENCY_CHANGE_EVENT } from "@/lib/player-currency-preference";
|
||||
import type { WalletBalanceData } from "@/types/api/wallet-balance";
|
||||
|
||||
const BALANCE_KEY = (currency: string) => ["wallet/balance", currency];
|
||||
|
||||
/** 独立路由 `/wallet/transfer-out` */
|
||||
export function TransferOutScreen() {
|
||||
const router = useRouter();
|
||||
const { activeCurrency: currency } = useActivePlayerCurrency();
|
||||
const [balance, setBalance] = useState<WalletBalanceData | null>(null);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const { mutate } = useSWRConfig();
|
||||
|
||||
const load = useCallback(async () => {
|
||||
const b = await getWalletBalance({ currency });
|
||||
setBalance(b);
|
||||
}, [currency]);
|
||||
const { data: balance, isLoading: loading } = useApiQuery(
|
||||
BALANCE_KEY(currency),
|
||||
() => getWalletBalance({ currency }),
|
||||
);
|
||||
|
||||
// 币种切换时 SWR key 自动变化,此处仅处理全局事件触发的刷新
|
||||
useEffect(() => {
|
||||
let c = false;
|
||||
void (async () => {
|
||||
try {
|
||||
await load();
|
||||
} finally {
|
||||
if (!c) setLoading(false);
|
||||
}
|
||||
})();
|
||||
return () => {
|
||||
c = true;
|
||||
};
|
||||
}, [load]);
|
||||
|
||||
useEffect(() => {
|
||||
const onCurrencyChange = () => void load();
|
||||
window.addEventListener(PLAYER_CURRENCY_CHANGE_EVENT, onCurrencyChange);
|
||||
return () => window.removeEventListener(PLAYER_CURRENCY_CHANGE_EVENT, onCurrencyChange);
|
||||
}, [load]);
|
||||
const onRefresh = () => void mutate(BALANCE_KEY(currency));
|
||||
window.addEventListener(PLAYER_CURRENCY_CHANGE_EVENT, onRefresh);
|
||||
return () => window.removeEventListener(PLAYER_CURRENCY_CHANGE_EVENT, onRefresh);
|
||||
}, [mutate, currency]);
|
||||
|
||||
const onSuccess = useCallback(async () => {
|
||||
await load();
|
||||
await mutate(BALANCE_KEY(currency));
|
||||
router.push("/wallet");
|
||||
}, [load, router]);
|
||||
}, [mutate, currency, router]);
|
||||
|
||||
if (loading && !balance) {
|
||||
return (
|
||||
|
||||
@@ -66,11 +66,13 @@ export function WalletScreen() {
|
||||
setLogsLoading(true);
|
||||
}
|
||||
try {
|
||||
const b = await getWalletBalance({ currency });
|
||||
// 并行请求余额和日志,避免瀑布式串行等待
|
||||
const [b, nextLogs] = await Promise.all([
|
||||
getWalletBalance({ currency }),
|
||||
loadLogs(1, false),
|
||||
]);
|
||||
if (cancelled) return;
|
||||
setBalance(b);
|
||||
const nextLogs = await loadLogs(1, false);
|
||||
if (cancelled) return;
|
||||
setLogs(nextLogs);
|
||||
} catch (e) {
|
||||
if (!cancelled) {
|
||||
@@ -93,9 +95,12 @@ export function WalletScreen() {
|
||||
setError(null);
|
||||
setLogsLoading(true);
|
||||
try {
|
||||
const b = await getWalletBalance({ currency });
|
||||
// 并行请求余额和日志
|
||||
const [b] = await Promise.all([
|
||||
getWalletBalance({ currency }),
|
||||
loadLogs(1, false),
|
||||
]);
|
||||
setBalance(b);
|
||||
await loadLogs(1, false);
|
||||
} catch (e) {
|
||||
setError(formatWalletClientError(e, t));
|
||||
} finally {
|
||||
|
||||
@@ -5,3 +5,4 @@ export { useWalletPolling, triggerWalletPollingAfterBet } from "./use-wallet-pol
|
||||
export { useWebSocketManager } from "./use-websocket-manager";
|
||||
export { usePlayerBalanceWs } from "./use-player-balance-ws";
|
||||
export { usePlayEffectiveWs } from "./use-play-effective-ws";
|
||||
export { useApiQuery } from "./use-api-query";
|
||||
|
||||
38
src/hooks/use-api-query.ts
Normal file
38
src/hooks/use-api-query.ts
Normal file
@@ -0,0 +1,38 @@
|
||||
"use client";
|
||||
|
||||
import useSWR, { type SWRConfiguration, type SWRResponse } from "swr";
|
||||
|
||||
/**
|
||||
* 通用 API 查询 Hook —— 基于 SWR 的薄包装。
|
||||
*
|
||||
* @param key SWR 缓存键。传 `null` 表示暂不请求(条件请求)。
|
||||
* @param fetcher 无参异步函数,通常由 `api/*.ts` 的函数包装而来。
|
||||
* @param options SWR 配置覆盖。
|
||||
*
|
||||
* @example
|
||||
* ```tsx
|
||||
* // 简单用法
|
||||
* const { data, error, isLoading } = useApiQuery(
|
||||
* currency ? ["wallet/balance", currency] : null,
|
||||
* () => getWalletBalance({ currency }),
|
||||
* );
|
||||
*
|
||||
* // 带自定义刷新间隔
|
||||
* const { data } = useApiQuery(
|
||||
* "draw/current",
|
||||
* () => getCurrentDraw(),
|
||||
* { refreshInterval: 5_000 },
|
||||
* );
|
||||
* ```
|
||||
*/
|
||||
export function useApiQuery<Data = unknown, Error = unknown>(
|
||||
key: string | readonly unknown[] | null,
|
||||
fetcher: () => Promise<Data>,
|
||||
options?: SWRConfiguration<Data, Error>,
|
||||
): SWRResponse<Data, Error> {
|
||||
return useSWR<Data, Error>(key, fetcher, {
|
||||
revalidateOnFocus: false,
|
||||
dedupingInterval: 2_000,
|
||||
...options,
|
||||
});
|
||||
}
|
||||
@@ -2,13 +2,13 @@
|
||||
|
||||
import { useCallback, useEffect, useRef } from "react";
|
||||
|
||||
import { getWalletBalance } from "@/api/wallet";
|
||||
import { getActivePlayerCurrencyFromStore } from "@/lib/player-currency";
|
||||
import { startWalletRefreshBurst } from "@/lib/wallet-refresh-burst";
|
||||
import { useNetworkConnectionStore } from "@/stores/network-connection-store";
|
||||
|
||||
const POLLING_INTERVAL_MS = 30_000; // 30秒轮询间隔
|
||||
const LIMITED_POLLING_DURATION_MS = 2 * 60 * 1000; // 2分钟限时轮询
|
||||
import {
|
||||
refreshWalletNow,
|
||||
startLimitedWalletPolling,
|
||||
startPersistentWalletPolling,
|
||||
stopWalletPolling,
|
||||
} from "@/lib/wallet-polling-utils";
|
||||
|
||||
export type UseWalletPollingReturn = {
|
||||
/** 开始钱包轮询(持续或限时) */
|
||||
@@ -29,14 +29,10 @@ export type UseWalletPollingReturn = {
|
||||
* 3. 开奖结果后的限时轮询(2分钟)
|
||||
*/
|
||||
export function useWalletPolling(): UseWalletPollingReturn {
|
||||
const store = useNetworkConnectionStore();
|
||||
const {
|
||||
walletPollingIntervalId,
|
||||
walletPollingExpiryAt,
|
||||
setWalletPollingIntervalId,
|
||||
setWalletPollingExpiryAt,
|
||||
clearWalletPolling,
|
||||
} = store;
|
||||
// 使用 selector 避免整 store 订阅导致无谓重渲染
|
||||
const walletPollingIntervalId = useNetworkConnectionStore(
|
||||
(s) => s.walletPollingIntervalId,
|
||||
);
|
||||
|
||||
const intervalIdRef = useRef<number | null>(walletPollingIntervalId);
|
||||
|
||||
@@ -45,79 +41,28 @@ export function useWalletPolling(): UseWalletPollingReturn {
|
||||
intervalIdRef.current = walletPollingIntervalId;
|
||||
}, [walletPollingIntervalId]);
|
||||
|
||||
// 刷新钱包余额
|
||||
const refreshWallet = useCallback(async () => {
|
||||
try {
|
||||
await getWalletBalance({ currency: getActivePlayerCurrencyFromStore() });
|
||||
// 触发全局刷新事件,让所有监听组件更新
|
||||
window.dispatchEvent(new Event("lottery-wallet-refresh"));
|
||||
} catch {
|
||||
// 静默处理错误,避免频繁报错
|
||||
}
|
||||
}, []);
|
||||
|
||||
// 开始钱包轮询
|
||||
// 开始钱包轮询(委托给共享工具函数)
|
||||
const startWalletPolling = useCallback(
|
||||
(options?: { limitedDuration?: boolean }) => {
|
||||
const { limitedDuration = false } = options ?? {};
|
||||
|
||||
// 先停止现有的轮询
|
||||
if (intervalIdRef.current) {
|
||||
window.clearInterval(intervalIdRef.current);
|
||||
intervalIdRef.current = null;
|
||||
}
|
||||
|
||||
// 立即执行一次刷新
|
||||
void refreshWallet();
|
||||
|
||||
// 设置轮询间隔
|
||||
const intervalId = window.setInterval(() => {
|
||||
// 检查限时轮询是否过期
|
||||
if (limitedDuration && walletPollingExpiryAt) {
|
||||
if (Date.now() > walletPollingExpiryAt) {
|
||||
// 过期,停止轮询
|
||||
clearWalletPolling();
|
||||
intervalIdRef.current = null;
|
||||
return;
|
||||
}
|
||||
}
|
||||
void refreshWallet();
|
||||
}, POLLING_INTERVAL_MS);
|
||||
|
||||
intervalIdRef.current = intervalId;
|
||||
setWalletPollingIntervalId(intervalId);
|
||||
|
||||
// 设置限时轮询的过期时间
|
||||
if (limitedDuration) {
|
||||
const expiryAt = Date.now() + LIMITED_POLLING_DURATION_MS;
|
||||
setWalletPollingExpiryAt(expiryAt);
|
||||
|
||||
// 设置一个定时器在过期时清理
|
||||
window.setTimeout(() => {
|
||||
if (intervalIdRef.current === intervalId) {
|
||||
clearWalletPolling();
|
||||
intervalIdRef.current = null;
|
||||
}
|
||||
}, LIMITED_POLLING_DURATION_MS + 1000); // 稍微延迟确保interval先执行检查
|
||||
startLimitedWalletPolling();
|
||||
} else {
|
||||
startPersistentWalletPolling();
|
||||
}
|
||||
},
|
||||
[
|
||||
walletPollingExpiryAt,
|
||||
refreshWallet,
|
||||
setWalletPollingIntervalId,
|
||||
setWalletPollingExpiryAt,
|
||||
clearWalletPolling,
|
||||
],
|
||||
[],
|
||||
);
|
||||
|
||||
// 停止钱包轮询
|
||||
const stopWalletPolling = useCallback(() => {
|
||||
const handleStop = useCallback(() => {
|
||||
if (intervalIdRef.current) {
|
||||
window.clearInterval(intervalIdRef.current);
|
||||
intervalIdRef.current = null;
|
||||
}
|
||||
clearWalletPolling();
|
||||
}, [clearWalletPolling]);
|
||||
stopWalletPolling();
|
||||
}, []);
|
||||
|
||||
// 组件卸载时清理
|
||||
useEffect(() => {
|
||||
@@ -130,8 +75,8 @@ export function useWalletPolling(): UseWalletPollingReturn {
|
||||
|
||||
return {
|
||||
startWalletPolling,
|
||||
stopWalletPolling,
|
||||
refreshWallet,
|
||||
stopWalletPolling: handleStop,
|
||||
refreshWallet: refreshWalletNow,
|
||||
isPolling: walletPollingIntervalId !== null,
|
||||
};
|
||||
}
|
||||
@@ -144,7 +89,7 @@ export function triggerWalletPollingAfterBet(): void {
|
||||
const store = useNetworkConnectionStore.getState();
|
||||
|
||||
if (store.mode === "polling" || store.mode === "offline") {
|
||||
startWalletRefreshBurst();
|
||||
startLimitedWalletPolling();
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
@@ -2,20 +2,22 @@
|
||||
|
||||
import { useCallback, useEffect, useRef, useState } from "react";
|
||||
|
||||
import { getWalletBalance } from "@/api/wallet";
|
||||
import { getActivePlayerCurrencyFromStore } from "@/lib/player-currency";
|
||||
import {
|
||||
disconnectLotteryEcho,
|
||||
getLotteryEcho,
|
||||
isLotteryReverbConfigured,
|
||||
} from "@/lib/lottery-echo";
|
||||
import {
|
||||
refreshWalletNow,
|
||||
startLimitedWalletPolling,
|
||||
startPersistentWalletPolling,
|
||||
stopWalletPolling as stopWalletPollingUtil,
|
||||
} from "@/lib/wallet-polling-utils";
|
||||
import {
|
||||
useNetworkConnectionStore,
|
||||
type NetworkMode,
|
||||
} from "@/stores/network-connection-store";
|
||||
|
||||
const POLLING_INTERVAL_MS = 30_000; // 30秒轮询间隔
|
||||
const WALLET_POLLING_DURATION_MS = 2 * 60 * 1000; // 2分钟限时轮询
|
||||
const RECONNECT_INTERVAL_MS = 5_000; // 5秒重连间隔
|
||||
const MAX_RECONNECT_ATTEMPTS = 10; // 最大重连次数
|
||||
|
||||
@@ -59,81 +61,45 @@ export type UseWebSocketManagerReturn = {
|
||||
* 5. 可选钱包余额轮询(`startWalletPolling`,下注等场景)
|
||||
*/
|
||||
export function useWebSocketManager(): UseWebSocketManagerReturn {
|
||||
const store = useNetworkConnectionStore();
|
||||
// 使用 selector 避免整 store 订阅导致无谓重渲染
|
||||
const mode = useNetworkConnectionStore((s) => s.mode);
|
||||
const reconnectAttempts = useNetworkConnectionStore((s) => s.reconnectAttempts);
|
||||
const setWebSocketConnected = useNetworkConnectionStore((s) => s.setWebSocketConnected);
|
||||
const setReconnecting = useNetworkConnectionStore((s) => s.setReconnecting);
|
||||
const incrementReconnectAttempts = useNetworkConnectionStore((s) => s.incrementReconnectAttempts);
|
||||
const resetReconnectAttempts = useNetworkConnectionStore((s) => s.resetReconnectAttempts);
|
||||
const setLastDisconnectedAt = useNetworkConnectionStore((s) => s.setLastDisconnectedAt);
|
||||
const switchToPollingMode = useNetworkConnectionStore((s) => s.switchToPollingMode);
|
||||
const switchToWebSocketMode = useNetworkConnectionStore((s) => s.switchToWebSocketMode);
|
||||
|
||||
const reconnectTimerRef = useRef<number | null>(null);
|
||||
const attemptReconnectRef = useRef<() => void>(() => {});
|
||||
/** 递增后重新创建 Echo 并绑定连接事件(供「恢复」与重连使用) */
|
||||
const [echoSession, setEchoSession] = useState(0);
|
||||
|
||||
const {
|
||||
mode,
|
||||
reconnectAttempts,
|
||||
setWebSocketConnected,
|
||||
setReconnecting,
|
||||
incrementReconnectAttempts,
|
||||
resetReconnectAttempts,
|
||||
setLastDisconnectedAt,
|
||||
switchToPollingMode,
|
||||
switchToWebSocketMode,
|
||||
clearWalletPolling,
|
||||
} = store;
|
||||
|
||||
// 刷新画作数据
|
||||
const refreshDraw = useCallback(async () => {
|
||||
// 由 useHallDrawLive 监听并拉取,避免与大厅重复请求 draw/current
|
||||
window.dispatchEvent(new Event("lottery-hall-refresh"));
|
||||
}, []);
|
||||
|
||||
// 刷新钱包余额
|
||||
const refreshWallet = useCallback(async () => {
|
||||
try {
|
||||
await getWalletBalance({ currency: getActivePlayerCurrencyFromStore() });
|
||||
// 触发全局刷新事件,让组件更新
|
||||
window.dispatchEvent(new Event("lottery-wallet-refresh"));
|
||||
} catch {
|
||||
// 静默处理错误
|
||||
}
|
||||
}, []);
|
||||
|
||||
// 钱包轮询
|
||||
// 钱包轮询(委托给共享工具函数,消除与 useWalletPolling 的重复逻辑)
|
||||
const startWalletPolling = useCallback(
|
||||
(options?: { limitedDuration?: boolean }) => {
|
||||
const { limitedDuration = false } = options ?? {};
|
||||
const s0 = useNetworkConnectionStore.getState();
|
||||
|
||||
const prevWalletId = s0.walletPollingIntervalId;
|
||||
if (prevWalletId !== null) {
|
||||
window.clearInterval(prevWalletId);
|
||||
}
|
||||
|
||||
void refreshWallet();
|
||||
|
||||
const intervalId = window.setInterval(() => {
|
||||
const s = useNetworkConnectionStore.getState();
|
||||
if (
|
||||
limitedDuration &&
|
||||
s.walletPollingExpiryAt !== null &&
|
||||
Date.now() > s.walletPollingExpiryAt
|
||||
) {
|
||||
s.clearWalletPolling();
|
||||
return;
|
||||
}
|
||||
void refreshWallet();
|
||||
}, POLLING_INTERVAL_MS);
|
||||
|
||||
s0.setWalletPollingIntervalId(intervalId);
|
||||
|
||||
if (limitedDuration) {
|
||||
s0.setWalletPollingExpiryAt(Date.now() + WALLET_POLLING_DURATION_MS);
|
||||
startLimitedWalletPolling();
|
||||
} else {
|
||||
startPersistentWalletPolling();
|
||||
}
|
||||
},
|
||||
[refreshWallet],
|
||||
[],
|
||||
);
|
||||
|
||||
// 停止钱包轮询
|
||||
const stopWalletPolling = useCallback(() => {
|
||||
clearWalletPolling();
|
||||
}, [clearWalletPolling]);
|
||||
stopWalletPollingUtil();
|
||||
}, []);
|
||||
|
||||
// 检测 Pusher 是否已真正 connected(非仅订阅频道)
|
||||
const isPusherConnected = useCallback((): boolean => {
|
||||
@@ -235,7 +201,7 @@ export function useWebSocketManager(): UseWebSocketManagerReturn {
|
||||
|
||||
const handleOffline = () => {
|
||||
// 网络断开
|
||||
store.setMode("offline");
|
||||
useNetworkConnectionStore.getState().setMode("offline");
|
||||
};
|
||||
|
||||
window.addEventListener("online", handleOnline);
|
||||
@@ -245,7 +211,7 @@ export function useWebSocketManager(): UseWebSocketManagerReturn {
|
||||
window.removeEventListener("online", handleOnline);
|
||||
window.removeEventListener("offline", handleOffline);
|
||||
};
|
||||
}, [mode, reconnect, store]);
|
||||
}, [mode, reconnect]);
|
||||
|
||||
// 初始化:绑定 Pusher 真实连接事件(勿仅凭 channel 对象存在判定已连接)
|
||||
useEffect(() => {
|
||||
@@ -349,6 +315,6 @@ export function useWebSocketManager(): UseWebSocketManagerReturn {
|
||||
startWalletPolling,
|
||||
stopWalletPolling,
|
||||
refreshDraw,
|
||||
refreshWallet,
|
||||
refreshWallet: refreshWalletNow,
|
||||
};
|
||||
}
|
||||
|
||||
70
src/lib/wallet-polling-utils.ts
Normal file
70
src/lib/wallet-polling-utils.ts
Normal file
@@ -0,0 +1,70 @@
|
||||
import { getWalletBalance } from "@/api/wallet";
|
||||
import { getActivePlayerCurrencyFromStore } from "@/lib/player-currency";
|
||||
import { useNetworkConnectionStore } from "@/stores/network-connection-store";
|
||||
|
||||
const POLLING_INTERVAL_MS = 30_000; // 30秒轮询间隔
|
||||
const LIMITED_POLLING_DURATION_MS = 2 * 60 * 1000; // 2分钟限时轮询
|
||||
|
||||
/**
|
||||
* 非 Hook 钱包刷新工具 —— 供 Hook 与非 React 上下文共用。
|
||||
*
|
||||
* 立即拉取一次余额,并触发 `lottery-wallet-refresh` 全局事件。
|
||||
*/
|
||||
export async function refreshWalletNow(): Promise<void> {
|
||||
try {
|
||||
await getWalletBalance({ currency: getActivePlayerCurrencyFromStore() });
|
||||
window.dispatchEvent(new Event("lottery-wallet-refresh"));
|
||||
} catch {
|
||||
// 静默处理错误,避免频繁报错
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 启动钱包限时轮询(2 分钟内每 30s 刷新一次)。
|
||||
* 先清除已有的钱包轮询计时器,避免泄漏。
|
||||
*/
|
||||
export function startLimitedWalletPolling(): void {
|
||||
const store = useNetworkConnectionStore.getState();
|
||||
store.clearWalletPolling();
|
||||
|
||||
void refreshWalletNow();
|
||||
|
||||
const expiryAt = Date.now() + LIMITED_POLLING_DURATION_MS;
|
||||
store.setWalletPollingExpiryAt(expiryAt);
|
||||
|
||||
const intervalId = window.setInterval(() => {
|
||||
const s = useNetworkConnectionStore.getState();
|
||||
if (
|
||||
s.walletPollingExpiryAt !== null &&
|
||||
Date.now() > s.walletPollingExpiryAt
|
||||
) {
|
||||
s.clearWalletPolling();
|
||||
return;
|
||||
}
|
||||
void refreshWalletNow();
|
||||
}, POLLING_INTERVAL_MS);
|
||||
|
||||
store.setWalletPollingIntervalId(intervalId);
|
||||
}
|
||||
|
||||
/**
|
||||
* 启动钱包持续轮询(直到手动停止)。
|
||||
* 先清除已有的钱包轮询计时器,避免泄漏。
|
||||
*/
|
||||
export function startPersistentWalletPolling(): void {
|
||||
const store = useNetworkConnectionStore.getState();
|
||||
store.clearWalletPolling();
|
||||
|
||||
void refreshWalletNow();
|
||||
|
||||
const intervalId = window.setInterval(() => {
|
||||
void refreshWalletNow();
|
||||
}, POLLING_INTERVAL_MS);
|
||||
|
||||
store.setWalletPollingIntervalId(intervalId);
|
||||
}
|
||||
|
||||
/** 停止钱包轮询 */
|
||||
export function stopWalletPolling(): void {
|
||||
useNetworkConnectionStore.getState().clearWalletPolling();
|
||||
}
|
||||
@@ -1,27 +1,12 @@
|
||||
import { useNetworkConnectionStore } from "@/stores/network-connection-store";
|
||||
|
||||
const WALLET_REFRESH_BURST_MS = 30_000;
|
||||
const WALLET_REFRESH_BURST_DURATION_MS = 2 * 60 * 1000;
|
||||
/**
|
||||
* @deprecated 请直接使用 `@/lib/wallet-polling-utils` 的 `startLimitedWalletPolling`。
|
||||
* 此文件保留向后兼容,内部已委托给共享工具函数。
|
||||
*/
|
||||
import { startLimitedWalletPolling } from "@/lib/wallet-polling-utils";
|
||||
|
||||
/**
|
||||
* 开奖等场景:立即刷新余额,并在限时内每 30s 派发 `lottery-wallet-refresh`(先清旧定时器,避免泄漏)。
|
||||
*/
|
||||
export function startWalletRefreshBurst(): void {
|
||||
const store = useNetworkConnectionStore.getState();
|
||||
store.clearWalletPolling();
|
||||
|
||||
window.dispatchEvent(new Event("lottery-wallet-refresh"));
|
||||
|
||||
const expiryAt = Date.now() + WALLET_REFRESH_BURST_DURATION_MS;
|
||||
store.setWalletPollingExpiryAt(expiryAt);
|
||||
|
||||
const intervalId = window.setInterval(() => {
|
||||
if (Date.now() > expiryAt) {
|
||||
useNetworkConnectionStore.getState().clearWalletPolling();
|
||||
return;
|
||||
}
|
||||
window.dispatchEvent(new Event("lottery-wallet-refresh"));
|
||||
}, WALLET_REFRESH_BURST_MS);
|
||||
|
||||
store.setWalletPollingIntervalId(intervalId);
|
||||
startLimitedWalletPolling();
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user