fix(folder): prevent stale file watch subscriptions in aux tabs

Ensure async watch setup is safely discarded after effect cleanup.

Use idempotent watcher release logic to avoid duplicate subscriptions and unbalanced stop calls.
This commit is contained in:
xintaofei
2026-04-14 16:43:35 +08:00
parent 9f1540129b
commit 90e8bb645a
2 changed files with 58 additions and 6 deletions

View File

@@ -1895,8 +1895,23 @@ export function FileTreeTab() {
if (!rootPath) return if (!rootPath) return
let unlisten: (() => void) | null = null let unlisten: (() => void) | null = null
let disposed = false
let watchStarted = false
let watchReleased = false
const normalizedRootPath = normalizeComparePath(rootPath) const normalizedRootPath = normalizeComparePath(rootPath)
const releaseWatch = () => {
if (watchReleased) return
watchReleased = true
if (unlisten) {
unlisten()
unlisten = null
}
if (watchStarted) {
void stopFileTreeWatch(rootPath)
}
}
const scheduleTreeRefresh = (refreshGitStatus: boolean) => { const scheduleTreeRefresh = (refreshGitStatus: boolean) => {
if (!isFileTreeTabActiveRef.current) { if (!isFileTreeTabActiveRef.current) {
pendingTreeRefreshRef.current = true pendingTreeRefreshRef.current = true
@@ -2027,13 +2042,18 @@ export function FileTreeTab() {
const setup = async () => { const setup = async () => {
try { try {
await startFileTreeWatch(rootPath) await startFileTreeWatch(rootPath)
watchStarted = true
} catch (error) { } catch (error) {
const message = error instanceof Error ? error.message : String(error) const message = error instanceof Error ? error.message : String(error)
toast.error(t("toasts.watchStartFailed"), { description: message }) toast.error(t("toasts.watchStartFailed"), { description: message })
} }
if (disposed) {
releaseWatch()
return
}
try { try {
unlisten = await subscribe<FileTreeChangedEvent>( const subscribedUnlisten = await subscribe<FileTreeChangedEvent>(
"folder://file-tree-changed", "folder://file-tree-changed",
(payload) => { (payload) => {
if ( if (
@@ -2109,6 +2129,12 @@ export function FileTreeTab() {
})() })()
} }
) )
if (disposed) {
subscribedUnlisten()
releaseWatch()
return
}
unlisten = subscribedUnlisten
} catch (error) { } catch (error) {
console.error("[FileTreeTab] failed to listen file watch event:", error) console.error("[FileTreeTab] failed to listen file watch event:", error)
} }
@@ -2117,6 +2143,7 @@ export function FileTreeTab() {
void setup() void setup()
return () => { return () => {
disposed = true
if (treeRefreshTimerRef.current) { if (treeRefreshTimerRef.current) {
clearTimeout(treeRefreshTimerRef.current) clearTimeout(treeRefreshTimerRef.current)
treeRefreshTimerRef.current = null treeRefreshTimerRef.current = null
@@ -2129,8 +2156,7 @@ export function FileTreeTab() {
pendingTreeRefreshRef.current = false pendingTreeRefreshRef.current = false
pendingTreeRefreshNeedsStatusRef.current = false pendingTreeRefreshNeedsStatusRef.current = false
pendingStatusRefreshRef.current = false pendingStatusRefreshRef.current = false
unlisten?.() releaseWatch()
void stopFileTreeWatch(rootPath)
} }
}, [fetchTree, folder?.path, openFilePreview, t]) }, [fetchTree, folder?.path, openFilePreview, t])

View File

@@ -615,8 +615,23 @@ export function GitChangesTab() {
if (!rootPath || !isChangesTabActive) return if (!rootPath || !isChangesTabActive) return
let unlisten: (() => void) | null = null let unlisten: (() => void) | null = null
let disposed = false
let watchStarted = false
let watchReleased = false
const normalizedRootPath = normalizeComparePath(rootPath) const normalizedRootPath = normalizeComparePath(rootPath)
const releaseWatch = () => {
if (watchReleased) return
watchReleased = true
if (unlisten) {
unlisten()
unlisten = null
}
if (watchStarted) {
void stopFileTreeWatch(rootPath)
}
}
const scheduleRefresh = () => { const scheduleRefresh = () => {
if (refreshTimerRef.current) { if (refreshTimerRef.current) {
clearTimeout(refreshTimerRef.current) clearTimeout(refreshTimerRef.current)
@@ -629,12 +644,17 @@ export function GitChangesTab() {
const setup = async () => { const setup = async () => {
try { try {
await startFileTreeWatch(rootPath) await startFileTreeWatch(rootPath)
watchStarted = true
} catch { } catch {
// ignore watch startup errors // ignore watch startup errors
} }
if (disposed) {
releaseWatch()
return
}
try { try {
unlisten = await subscribe<FileTreeChangedEvent>( const subscribedUnlisten = await subscribe<FileTreeChangedEvent>(
"folder://file-tree-changed", "folder://file-tree-changed",
(payload) => { (payload) => {
if ( if (
@@ -646,6 +666,12 @@ export function GitChangesTab() {
scheduleRefresh() scheduleRefresh()
} }
) )
if (disposed) {
subscribedUnlisten()
releaseWatch()
return
}
unlisten = subscribedUnlisten
} catch { } catch {
// ignore listen errors // ignore listen errors
} }
@@ -654,12 +680,12 @@ export function GitChangesTab() {
void setup() void setup()
return () => { return () => {
disposed = true
if (refreshTimerRef.current) { if (refreshTimerRef.current) {
clearTimeout(refreshTimerRef.current) clearTimeout(refreshTimerRef.current)
refreshTimerRef.current = null refreshTimerRef.current = null
} }
unlisten?.() releaseWatch()
void stopFileTreeWatch(rootPath)
} }
}, [fetchChanges, folder?.path, isChangesTabActive]) }, [fetchChanges, folder?.path, isChangesTabActive])