Compare commits

...

2 Commits

Author SHA1 Message Date
Tunglies
b8fbabae04
fix: frontend memory leaks — Monaco dispose, TQ cache eviction, useEffect cleanup
- Dispose Monaco editor instances on dialog close to prevent cycle leak
- Replace gcTime: Infinity with finite TTLs and evict orphaned subscription queryKeys
- Add missing useEffect cleanup for timers, move setTimeout out of useMemo
2026-04-05 23:10:45 +08:00
renovate[bot]
2c766e1ada
chore(deps): update dependency @tauri-apps/plugin-updater to v2.10.1 (#6726)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-04-05 14:24:49 +00:00
13 changed files with 108 additions and 31 deletions

View File

@ -54,7 +54,7 @@
"@tauri-apps/plugin-http": "~2.5.7",
"@tauri-apps/plugin-process": "^2.3.1",
"@tauri-apps/plugin-shell": "2.3.5",
"@tauri-apps/plugin-updater": "2.10.0",
"@tauri-apps/plugin-updater": "2.10.1",
"ahooks": "^3.9.6",
"cidr-block": "^2.3.0",
"dayjs": "1.11.20",

10
pnpm-lock.yaml generated
View File

@ -69,8 +69,8 @@ importers:
specifier: 2.3.5
version: 2.3.5
'@tauri-apps/plugin-updater':
specifier: 2.10.0
version: 2.10.0
specifier: 2.10.1
version: 2.10.1
ahooks:
specifier: ^3.9.6
version: 3.9.7(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
@ -1562,8 +1562,8 @@ packages:
'@tauri-apps/plugin-shell@2.3.5':
resolution: {integrity: sha512-jewtULhiQ7lI7+owCKAjc8tYLJr92U16bPOeAa472LHJdgaibLP83NcfAF2e+wkEcA53FxKQAZ7byDzs2eeizg==}
'@tauri-apps/plugin-updater@2.10.0':
resolution: {integrity: sha512-ljN8jPlnT0aSn8ecYhuBib84alxfMx6Hc8vJSKMJyzGbTPFZAC44T2I1QNFZssgWKrAlofvJqCC6Rr472JWfkQ==}
'@tauri-apps/plugin-updater@2.10.1':
resolution: {integrity: sha512-NFYMg+tWOZPJdzE/PpFj2qfqwAWwNS3kXrb1tm1gnBJ9mYzZ4WDRrwy8udzWoAnfGCHLuePNLY1WVCNHnh3eRA==}
'@tybys/wasm-util@0.10.1':
resolution: {integrity: sha512-9tTaPJLSiejZKx+Bmog4uSubteqTvFrVrURwkmHixBo0G4seD0zUxp98E1DzUBJxLQ3NPwXrGKDiVjwx/DpPsg==}
@ -5059,7 +5059,7 @@ snapshots:
dependencies:
'@tauri-apps/api': 2.10.1
'@tauri-apps/plugin-updater@2.10.0':
'@tauri-apps/plugin-updater@2.10.1':
dependencies:
'@tauri-apps/api': 2.10.1

View File

@ -444,6 +444,12 @@ export const CurrentProxyCard = () => {
[setState],
)
useEffect(() => {
return () => {
if (timeoutRef.current) clearTimeout(timeoutRef.current)
}
}, [])
// 处理代理组变更
const handleGroupChange = useCallback(
(event: SelectChangeEvent<string>) => {

View File

@ -425,7 +425,7 @@ function useIPInfo() {
queryKey: [IP_INFO_CACHE_KEY],
queryFn: getIpInfo,
staleTime: Infinity,
gcTime: Infinity,
gcTime: 60 * 60 * 1000,
refetchOnWindowFocus: false,
refetchOnReconnect: false,
retry: 1,

View File

@ -168,6 +168,13 @@ export const EditorViewer = ({
}
}, [open, syncMaximizedState])
useEffect(() => {
return () => {
editorRef.current?.dispose()
editorRef.current = null
}
}, [])
return (
<Dialog
open={open}

View File

@ -34,11 +34,13 @@ import {
requestIdleCallback,
} from 'foxact/request-idle-callback'
import yaml from 'js-yaml'
import type { editor } from 'monaco-editor'
import {
startTransition,
useCallback,
useEffect,
useMemo,
useRef,
useState,
} from 'react'
import { Controller, useForm } from 'react-hook-form'
@ -149,6 +151,7 @@ export const GroupsEditorViewer = (props: Props) => {
[t],
)
const themeMode = useThemeMode()
const editorRef = useRef<editor.IStandaloneCodeEditor | null>(null)
const [prevData, setPrevData] = useState('')
const [currData, setCurrData] = useState('')
const [visualization, setVisualization] = useState(true)
@ -481,6 +484,13 @@ export const GroupsEditorViewer = (props: Props) => {
getInterfaceNameList()
}, [fetchContent, fetchProfile, getInterfaceNameList, open])
useEffect(() => {
return () => {
editorRef.current?.dispose()
editorRef.current = null
}
}, [])
const validateGroup = () => {
const group = formIns.getValues()
if (group.name === '') {
@ -1105,6 +1115,9 @@ export const GroupsEditorViewer = (props: Props) => {
language="yaml"
value={currData}
theme={themeMode === 'light' ? 'light' : 'vs-dark'}
onMount={(editorInstance) => {
editorRef.current = editorInstance
}}
options={{
tabSize: 2, // 根据语言类型设置缩进大小
minimap: {

View File

@ -19,7 +19,7 @@ import {
import { open } from '@tauri-apps/plugin-shell'
import { useLockFn } from 'ahooks'
import dayjs from 'dayjs'
import { useCallback, useEffect, useReducer, useState } from 'react'
import { useCallback, useEffect, useReducer, useRef, useState } from 'react'
import { useTranslation } from 'react-i18next'
import { ConfirmViewer } from '@/components/profile/confirm-viewer'
@ -96,7 +96,11 @@ export const ProfileItem = (props: Props) => {
// 新增状态:是否显示下次更新时间
const [showNextUpdate, setShowNextUpdate] = useState(false)
const showNextUpdateRef = useRef(false)
const [nextUpdateTime, setNextUpdateTime] = useState('')
const refreshTimeoutRef = useRef<ReturnType<typeof setTimeout> | undefined>(
undefined,
)
const { uid, name = 'Profile', extra, updated = 0, option } = itemData
@ -178,6 +182,10 @@ export const ProfileItem = (props: Props) => {
setShowNextUpdate(!showNextUpdate)
}
useEffect(() => {
showNextUpdateRef.current = showNextUpdate
}, [showNextUpdate])
// 当组件加载或更新间隔变化时更新下次更新时间
useEffect(() => {
if (showNextUpdate) {
@ -192,19 +200,18 @@ export const ProfileItem = (props: Props) => {
// 订阅定时器更新事件
useEffect(() => {
let refreshTimeout: number | undefined
// 处理定时器更新事件 - 这个事件专门用于通知定时器变更
const handleTimerUpdate = (event: Event) => {
const source = event as CustomEvent<string> & { payload?: string }
const updatedUid = source.detail ?? source.payload
// 只有当更新的是当前配置时才刷新显示
if (updatedUid === itemData.uid && showNextUpdate) {
if (updatedUid === itemData.uid && showNextUpdateRef.current) {
debugLog(`收到定时器更新事件: uid=${updatedUid}`)
if (refreshTimeout !== undefined) {
clearTimeout(refreshTimeout)
if (refreshTimeoutRef.current !== undefined) {
clearTimeout(refreshTimeoutRef.current)
}
refreshTimeout = window.setTimeout(() => {
refreshTimeoutRef.current = window.setTimeout(() => {
fetchNextUpdateTime(true)
}, 1000)
}
@ -214,13 +221,13 @@ export const ProfileItem = (props: Props) => {
window.addEventListener('verge://timer-updated', handleTimerUpdate)
return () => {
if (refreshTimeout !== undefined) {
clearTimeout(refreshTimeout)
if (refreshTimeoutRef.current !== undefined) {
clearTimeout(refreshTimeoutRef.current)
}
// 清理事件监听
window.removeEventListener('verge://timer-updated', handleTimerUpdate)
}
}, [fetchNextUpdateTime, itemData.uid, showNextUpdate])
}, [fetchNextUpdateTime, itemData.uid])
// local file mode
// remote file mode

View File

@ -27,11 +27,13 @@ import {
} from '@mui/material'
import { useLockFn } from 'ahooks'
import yaml from 'js-yaml'
import type { editor } from 'monaco-editor'
import {
startTransition,
useCallback,
useEffect,
useMemo,
useRef,
useState,
} from 'react'
import { useTranslation } from 'react-i18next'
@ -56,6 +58,7 @@ export const ProxiesEditorViewer = (props: Props) => {
const { profileUid, property, open, onClose, onSave } = props
const { t } = useTranslation()
const themeMode = useThemeMode()
const editorRef = useRef<editor.IStandaloneCodeEditor | null>(null)
const [prevData, setPrevData] = useState('')
const [currData, setCurrData] = useState('')
const [visualization, setVisualization] = useState(true)
@ -343,6 +346,13 @@ export const ProxiesEditorViewer = (props: Props) => {
fetchProfile()
}, [fetchContent, fetchProfile, open])
useEffect(() => {
return () => {
editorRef.current?.dispose()
editorRef.current = null
}
}, [])
const handleSave = useLockFn(async () => {
try {
await saveProfileFile(property, currData)
@ -469,6 +479,9 @@ export const ProxiesEditorViewer = (props: Props) => {
language="yaml"
value={currData}
theme={themeMode === 'light' ? 'light' : 'vs-dark'}
onMount={(editorInstance) => {
editorRef.current = editorInstance
}}
options={{
tabSize: 2, // 根据语言类型设置缩进大小
minimap: {

View File

@ -29,11 +29,13 @@ import {
} from '@mui/material'
import { useLockFn } from 'ahooks'
import yaml from 'js-yaml'
import type { editor } from 'monaco-editor'
import {
startTransition,
useCallback,
useEffect,
useMemo,
useRef,
useState,
} from 'react'
import { useTranslation } from 'react-i18next'
@ -251,6 +253,8 @@ export const RulesEditorViewer = (props: Props) => {
const { t } = useTranslation()
const themeMode = useThemeMode()
const editorRef = useRef<editor.IStandaloneCodeEditor | null>(null)
const [prevData, setPrevData] = useState('')
const [currData, setCurrData] = useState('')
const [visualization, setVisualization] = useState(true)
@ -536,6 +540,13 @@ export const RulesEditorViewer = (props: Props) => {
fetchProfile()
}, [fetchContent, fetchProfile, open])
useEffect(() => {
return () => {
editorRef.current?.dispose()
editorRef.current = null
}
}, [])
const validateRule = () => {
if ((ruleType.required ?? true) && !ruleContent) {
throw new Error(
@ -770,6 +781,9 @@ export const RulesEditorViewer = (props: Props) => {
language="yaml"
value={currData}
theme={themeMode === 'light' ? 'light' : 'vs-dark'}
onMount={(editorInstance) => {
editorRef.current = editorInstance
}}
options={{
tabSize: 2, // 根据语言类型设置缩进大小
minimap: {

View File

@ -16,6 +16,7 @@ import {
import { invoke } from '@tauri-apps/api/core'
import { useLockFn } from 'ahooks'
import yaml from 'js-yaml'
import type { editor } from 'monaco-editor'
import type { Ref } from 'react'
import {
useCallback,
@ -189,6 +190,7 @@ export function DnsViewer({ ref }: { ref?: Ref<DialogRef> }) {
const [open, setOpen] = useState(false)
const [visualization, setVisualization] = useState(true)
const skipYamlSyncRef = useRef(false)
const editorRef = useRef<editor.IStandaloneCodeEditor | null>(null)
const [values, setValues] = useState<{
enable: boolean
listen: string
@ -453,6 +455,13 @@ export function DnsViewer({ ref }: { ref?: Ref<DialogRef> }) {
}
}, [visualization])
useEffect(() => {
return () => {
editorRef.current?.dispose()
editorRef.current = null
}
}, [])
const initDnsConfig = useCallback(async () => {
try {
const dnsConfigExists = await invoke<boolean>(
@ -1057,6 +1066,9 @@ export function DnsViewer({ ref }: { ref?: Ref<DialogRef> }) {
value={yamlContent}
theme={themeMode === 'light' ? 'light' : 'vs-dark'}
className="flex-grow"
onMount={(editorInstance) => {
editorRef.current = editorInstance
}}
options={{
tabSize: 2,
minimap: {

View File

@ -42,7 +42,7 @@ export const useIconCache = ({
refetchOnWindowFocus: false,
refetchOnReconnect: false,
staleTime: Infinity,
gcTime: Infinity,
gcTime: 30 * 60 * 1000,
retry: 2,
})

View File

@ -93,7 +93,7 @@ export const useMihomoWsSubscription = <T>(
subscriptionCacheKey ?? '$sub$__disabled__',
]) ?? fallbackData,
staleTime: Infinity,
gcTime: Infinity,
gcTime: 30_000,
enabled: subscriptionCacheKey !== null,
})
@ -243,8 +243,11 @@ export const useMihomoWsSubscription = <T>(
}, [subscriptionCacheKey])
const refresh = useCallback(() => {
if (subscriptionCacheKey) {
queryClient.removeQueries({ queryKey: [subscriptionCacheKey] })
}
setDate(Date.now())
}, [setDate])
}, [queryClient, subscriptionCacheKey, setDate])
return { response, refresh, subscriptionCacheKey, wsRef }
}

View File

@ -309,20 +309,22 @@ export const useCustomTheme = () => {
styleElement.innerHTML = effectiveInjectedCss + globalStyles
}
const { palette } = muiTheme
setTimeout(() => {
const dom = document.querySelector('#Gradient2')
if (dom) {
dom.innerHTML = `
<stop offset="0%" stop-color="${palette.primary.main}" />
<stop offset="80%" stop-color="${palette.primary.dark}" />
<stop offset="100%" stop-color="${palette.primary.dark}" />
`
}
}, 0)
return muiTheme
}, [mode, theme_setting, userBackgroundImage, hasUserBackground])
useEffect(() => {
const id = setTimeout(() => {
const dom = document.querySelector('#Gradient2')
if (dom) {
dom.innerHTML = `
<stop offset="0%" stop-color="${theme.palette.primary.main}" />
<stop offset="80%" stop-color="${theme.palette.primary.dark}" />
<stop offset="100%" stop-color="${theme.palette.primary.dark}" />
`
}
}, 0)
return () => clearTimeout(id)
}, [theme.palette.primary.main, theme.palette.primary.dark])
return { theme }
}