diff --git a/src/components/proxy/proxy-item-mini.tsx b/src/components/proxy/proxy-item-mini.tsx index 532dd659..a6e6e660 100644 --- a/src/components/proxy/proxy-item-mini.tsx +++ b/src/components/proxy/proxy-item-mini.tsx @@ -6,7 +6,7 @@ import { useTranslation } from "react-i18next"; import { BaseLoading } from "@/components/base"; import { useVerge } from "@/hooks/use-verge"; -import delayManager from "@/services/delay"; +import delayManager, { DelayUpdate } from "@/services/delay"; interface Props { group: IProxyGroupItem; @@ -24,15 +24,17 @@ export const ProxyItemMini = (props: Props) => { const presetList = ["DIRECT", "REJECT", "REJECT-DROP", "PASS", "COMPATIBLE"]; const isPreset = presetList.includes(proxy.name); - // -1/<=0 为 不显示 - // -2 为 loading - const [delay, setDelay] = useReducer((_: number, value: number) => value, -1); + // -1/<=0 为不显示,-2 为 loading + const [delayState, setDelayState] = useReducer( + (_: DelayUpdate, next: DelayUpdate) => next, + { delay: -1, updatedAt: 0 }, + ); const { verge } = useVerge(); const timeout = verge?.default_latency_timeout || 10000; useEffect(() => { if (isPreset) return; - delayManager.setListener(proxy.name, group.name, setDelay); + delayManager.setListener(proxy.name, group.name, setDelayState); return () => { delayManager.removeListener(proxy.name, group.name); @@ -41,7 +43,32 @@ export const ProxyItemMini = (props: Props) => { const updateDelay = useCallback(() => { if (!proxy) return; - setDelay(delayManager.getDelayFix(proxy, group.name)); + const cachedUpdate = delayManager.getDelayUpdate(proxy.name, group.name); + if (cachedUpdate) { + setDelayState({ ...cachedUpdate }); + return; + } + + const fallbackDelay = delayManager.getDelayFix(proxy, group.name); + if (fallbackDelay === -1) { + setDelayState({ delay: -1, updatedAt: 0 }); + return; + } + + let updatedAt = 0; + const history = proxy.history; + if (history && history.length > 0) { + const lastRecord = history[history.length - 1]; + const parsed = Date.parse(lastRecord.time); + if (!Number.isNaN(parsed)) { + updatedAt = parsed; + } + } + + setDelayState({ + delay: fallbackDelay, + updatedAt, + }); }, [proxy, group.name]); useEffect(() => { @@ -49,10 +76,14 @@ export const ProxyItemMini = (props: Props) => { }, [updateDelay]); const onDelay = useLockFn(async () => { - setDelay(-2); - setDelay(await delayManager.checkDelay(proxy.name, group.name, timeout)); + setDelayState({ delay: -2, updatedAt: Date.now() }); + setDelayState( + await delayManager.checkDelay(proxy.name, group.name, timeout), + ); }); + const delayValue = delayState.delay; + return ( { }, ({ palette: { mode, primary } }) => { const bgcolor = mode === "light" ? "#ffffff" : "#24252f"; - const showDelay = delay > 0; + const showDelay = delayValue > 0; const selectColor = mode === "light" ? primary.main : primary.light; return { @@ -181,13 +212,13 @@ export const ProxyItemMini = (props: Props) => { - {delay === -2 && ( + {delayValue === -2 && ( )} - {!proxy.provider && delay !== -2 && ( - // provider的节点不支持检测 + {!proxy.provider && delayValue !== -2 && ( + // provider 的节点不支持检测 { @@ -196,7 +227,7 @@ export const ProxyItemMini = (props: Props) => { onDelay(); }} sx={({ palette }) => ({ - display: "none", // hover才显示 + display: "none", // hover 时显示 ":hover": { bgcolor: alpha(palette.primary.main, 0.15) }, })} > @@ -204,7 +235,7 @@ export const ProxyItemMini = (props: Props) => { )} - {delay >= 0 && ( + {delayValue >= 0 && ( // 显示延迟 { e.stopPropagation(); onDelay(); }} - color={delayManager.formatDelayColor(delay, timeout)} + color={delayManager.formatDelayColor(delayValue, timeout)} sx={({ palette }) => !proxy.provider ? { ":hover": { bgcolor: alpha(palette.primary.main, 0.15) } } : {} } > - {delayManager.formatDelay(delay, timeout)} + {delayManager.formatDelay(delayValue, timeout)} )} - {proxy.type !== "Direct" && delay !== -2 && delay < 0 && selected && ( - // 展示已选择的icon - - )} + {proxy.type !== "Direct" && + delayValue !== -2 && + delayValue < 0 && + selected && ( + // 展示已选择的 icon + + )} {group.fixed && group.fixed === proxy.name && ( - // 展示fixed状态 + // 展示 fixed 状态 { const presetList = ["DIRECT", "REJECT", "REJECT-DROP", "PASS", "COMPATIBLE"]; const isPreset = presetList.includes(proxy.name); - // -1/<=0 为 不显示 - // -2 为 loading - const [delay, setDelay] = useReducer((_: number, value: number) => value, -1); + // -1/<=0 为不显示,-2 为 loading + const [delayState, setDelayState] = useReducer( + (_: DelayUpdate, next: DelayUpdate) => next, + { delay: -1, updatedAt: 0 }, + ); const { verge } = useVerge(); const timeout = verge?.default_latency_timeout || 10000; + useEffect(() => { if (isPreset) return; - delayManager.setListener(proxy.name, group.name, setDelay); + delayManager.setListener(proxy.name, group.name, setDelayState); return () => { delayManager.removeListener(proxy.name, group.name); @@ -65,7 +68,32 @@ export const ProxyItem = (props: Props) => { const updateDelay = useCallback(() => { if (!proxy) return; - setDelay(delayManager.getDelayFix(proxy, group.name)); + const cachedUpdate = delayManager.getDelayUpdate(proxy.name, group.name); + if (cachedUpdate) { + setDelayState({ ...cachedUpdate }); + return; + } + + const fallbackDelay = delayManager.getDelayFix(proxy, group.name); + if (fallbackDelay === -1) { + setDelayState({ delay: -1, updatedAt: 0 }); + return; + } + + let updatedAt = 0; + const history = proxy.history; + if (history && history.length > 0) { + const lastRecord = history[history.length - 1]; + const parsed = Date.parse(lastRecord.time); + if (!Number.isNaN(parsed)) { + updatedAt = parsed; + } + } + + setDelayState({ + delay: fallbackDelay, + updatedAt, + }); }, [proxy, group.name]); useEffect(() => { @@ -73,10 +101,14 @@ export const ProxyItem = (props: Props) => { }, [updateDelay]); const onDelay = useLockFn(async () => { - setDelay(-2); - setDelay(await delayManager.checkDelay(proxy.name, group.name, timeout)); + setDelayState({ delay: -2, updatedAt: Date.now() }); + setDelayState( + await delayManager.checkDelay(proxy.name, group.name, timeout), + ); }); + const delayValue = delayState.delay; + return ( { ({ palette: { mode, primary } }) => { const bgcolor = mode === "light" ? "#ffffff" : "#24252f"; const selectColor = mode === "light" ? primary.main : primary.light; - const showDelay = delay > 0; + const showDelay = delayValue > 0; return { "&:hover .the-check": { display: !showDelay ? "block" : "none" }, @@ -145,14 +177,14 @@ export const ProxyItem = (props: Props) => { display: isPreset ? "none" : "", }} > - {delay === -2 && ( + {delayValue === -2 && ( )} - {!proxy.provider && delay !== -2 && ( - // provider的节点不支持检测 + {!proxy.provider && delayValue !== -2 && ( + // provider 的节点不支持检测 { @@ -161,7 +193,7 @@ export const ProxyItem = (props: Props) => { onDelay(); }} sx={({ palette }) => ({ - display: "none", // hover才显示 + display: "none", // hover 时显示 ":hover": { bgcolor: alpha(palette.primary.main, 0.15) }, })} > @@ -169,7 +201,7 @@ export const ProxyItem = (props: Props) => { )} - {delay > 0 && ( + {delayValue > 0 && ( // 显示延迟 { e.stopPropagation(); onDelay(); }} - color={delayManager.formatDelayColor(delay, timeout)} + color={delayManager.formatDelayColor(delayValue, timeout)} sx={({ palette }) => !proxy.provider ? { ":hover": { bgcolor: alpha(palette.primary.main, 0.15) } } : {} } > - {delayManager.formatDelay(delay, timeout)} + {delayManager.formatDelay(delayValue, timeout)} )} - {delay !== -2 && delay <= 0 && selected && ( - // 展示已选择的icon + {delayValue !== -2 && delayValue <= 0 && selected && ( + // 展示已选择的 icon `${group ?? ""}::${name}`; +export interface DelayUpdate { + delay: number; + elapsed?: number; + updatedAt: number; +} + +const CACHE_TTL = 30 * 60 * 1000; + class DelayManager { - private cache = new Map(); + private cache = new Map(); private urlMap = new Map(); - // 每个item的监听 - private listenerMap = new Map void>(); + // 每个节点的监听 + private listenerMap = new Map void>(); // 每个分组的监听 private groupListenerMap = new Map void>(); + private pendingItemUpdates = new Map(); + private pendingGroupUpdates = new Set(); + private itemFlushScheduled = false; + private groupFlushScheduled = false; + + private scheduleItemFlush() { + if (this.itemFlushScheduled) return; + this.itemFlushScheduled = true; + + const run = () => { + this.itemFlushScheduled = false; + const updates = this.pendingItemUpdates; + this.pendingItemUpdates = new Map(); + + updates.forEach((queue, key) => { + const listener = this.listenerMap.get(key); + if (!listener) return; + + queue.forEach((update) => { + try { + listener(update); + } catch (error) { + console.error( + `[DelayManager] 通知节点延迟监听器失败: ${key}`, + error, + ); + } + }); + }); + }; + + if (typeof window !== "undefined") { + if (typeof window.requestAnimationFrame === "function") { + window.requestAnimationFrame(run); + return; + } + if (typeof window.setTimeout === "function") { + window.setTimeout(run, 0); + return; + } + } + + Promise.resolve().then(run); + } + + private scheduleGroupFlush() { + if (this.groupFlushScheduled) return; + this.groupFlushScheduled = true; + + const run = () => { + this.groupFlushScheduled = false; + const groups = this.pendingGroupUpdates; + this.pendingGroupUpdates = new Set(); + + groups.forEach((group) => { + const listener = this.groupListenerMap.get(group); + if (!listener) return; + try { + listener(); + } catch (error) { + console.error( + `[DelayManager] 通知分组延迟监听器失败: ${group}`, + error, + ); + } + }); + }; + + if (typeof window !== "undefined") { + if (typeof window.requestAnimationFrame === "function") { + window.requestAnimationFrame(run); + return; + } + if (typeof window.setTimeout === "function") { + window.setTimeout(run, 0); + return; + } + } + + Promise.resolve().then(run); + } + + private queueGroupNotification(group: string) { + this.pendingGroupUpdates.add(group); + this.scheduleGroupFlush(); + } + setUrl(group: string, url: string) { console.log(`[DelayManager] 设置测试URL,组: ${group}, URL: ${url}`); this.urlMap.set(group, url); @@ -26,7 +121,11 @@ class DelayManager { return url || "https://cp.cloudflare.com/generate_204"; } - setListener(name: string, group: string, listener: (time: number) => void) { + setListener( + name: string, + group: string, + listener: (update: DelayUpdate) => void, + ) { const key = hashKey(name, group); this.listenerMap.set(key, listener); } @@ -44,34 +143,60 @@ class DelayManager { this.groupListenerMap.delete(group); } - setDelay(name: string, group: string, delay: number) { + setDelay( + name: string, + group: string, + delay: number, + meta?: { elapsed?: number }, + ): DelayUpdate { const key = hashKey(name, group); console.log( `[DelayManager] 设置延迟,代理: ${name}, 组: ${group}, 延迟: ${delay}`, ); + const update: DelayUpdate = { + delay, + elapsed: meta?.elapsed, + updatedAt: Date.now(), + }; - this.cache.set(key, [delay, Date.now()]); - const listener = this.listenerMap.get(key); - if (listener) listener(delay); + this.cache.set(key, update); + + const queue = this.pendingItemUpdates.get(key); + if (queue) { + queue.push(update); + } else { + this.pendingItemUpdates.set(key, [update]); + } + this.scheduleItemFlush(); + + return update; + } + + getDelayUpdate(name: string, group: string) { + const key = hashKey(name, group); + const entry = this.cache.get(key); + if (!entry) return undefined; + + if (Date.now() - entry.updatedAt > CACHE_TTL) { + this.cache.delete(key); + return undefined; + } + + return { ...entry }; } getDelay(name: string, group: string) { - const key = hashKey(name, group); - const val = this.cache.get(key); - if (!val) return -1; - - // 缓存30分钟 - if (Date.now() - val[1] > 30 * 60 * 1000) { - return -1; - } - return val[0]; + const update = this.getDelayUpdate(name, group); + return update ? update.delay : -1; } /// 暂时修复provider的节点延迟排序的问题 getDelayFix(proxy: IProxyItem, group: string) { if (!proxy.provider) { - const delay = this.getDelay(proxy.name, group); - if (delay >= 0 || delay === -2) return delay; + const update = this.getDelayUpdate(proxy.name, group); + if (update && (update.delay >= 0 || update.delay === -2)) { + return update.delay; + } } // 添加 history 属性的安全检查 @@ -82,7 +207,11 @@ class DelayManager { return -1; } - async checkDelay(name: string, group: string, timeout: number) { + async checkDelay( + name: string, + group: string, + timeout: number, + ): Promise { console.log( `[DelayManager] 开始测试延迟,代理: ${name}, 组: ${group}, 超时: ${timeout}ms`, ); @@ -91,16 +220,16 @@ class DelayManager { this.setDelay(name, group, -2); let delay = -1; + let elapsed = 0; + + const startTime = Date.now(); try { const url = this.getUrl(group); console.log(`[DelayManager] 调用API测试延迟,代理: ${name}, URL: ${url}`); - // 记录开始时间,用于计算实际延迟 - const startTime = Date.now(); - // 设置超时处理, delay = 0 为超时 - const timeoutPromise = new Promise((resolve, _) => { + const timeoutPromise = new Promise((resolve) => { setTimeout(() => resolve({ delay: 0 }), timeout); }); @@ -117,6 +246,7 @@ class DelayManager { } delay = result.delay; + elapsed = elapsedTime; console.log( `[DelayManager] 延迟测试完成,代理: ${name}, 结果: ${delay}ms`, ); @@ -125,10 +255,10 @@ class DelayManager { await new Promise((resolve) => setTimeout(resolve, 500)); console.error(`[DelayManager] 延迟测试出错,代理: ${name}`, error); delay = 1e6; // error + elapsed = Date.now() - startTime; } - this.setDelay(name, group, delay); - return delay; + return this.setDelay(name, group, delay, { elapsed }); } async checkListDelay( @@ -165,7 +295,9 @@ class DelayManager { } await this.checkDelay(currName, group, timeout); - if (listener) listener(); + if (listener) { + this.queueGroupNotification(group); + } } catch (error) { console.error( `[DelayManager] 批量测试单个代理出错,代理: ${currName}`,