From 90e8bb645af929cb56e407fb9aef1bffa1f50509 Mon Sep 17 00:00:00 2001 From: xintaofei Date: Tue, 14 Apr 2026 16:43:35 +0800 Subject: [PATCH] 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. --- .../layout/aux-panel-file-tree-tab.tsx | 32 +++++++++++++++++-- .../layout/aux-panel-git-changes-tab.tsx | 32 +++++++++++++++++-- 2 files changed, 58 insertions(+), 6 deletions(-) diff --git a/src/components/layout/aux-panel-file-tree-tab.tsx b/src/components/layout/aux-panel-file-tree-tab.tsx index b2e7871..4f410ff 100644 --- a/src/components/layout/aux-panel-file-tree-tab.tsx +++ b/src/components/layout/aux-panel-file-tree-tab.tsx @@ -1895,8 +1895,23 @@ export function FileTreeTab() { if (!rootPath) return let unlisten: (() => void) | null = null + let disposed = false + let watchStarted = false + let watchReleased = false 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) => { if (!isFileTreeTabActiveRef.current) { pendingTreeRefreshRef.current = true @@ -2027,13 +2042,18 @@ export function FileTreeTab() { const setup = async () => { try { await startFileTreeWatch(rootPath) + watchStarted = true } catch (error) { const message = error instanceof Error ? error.message : String(error) toast.error(t("toasts.watchStartFailed"), { description: message }) } + if (disposed) { + releaseWatch() + return + } try { - unlisten = await subscribe( + const subscribedUnlisten = await subscribe( "folder://file-tree-changed", (payload) => { if ( @@ -2109,6 +2129,12 @@ export function FileTreeTab() { })() } ) + if (disposed) { + subscribedUnlisten() + releaseWatch() + return + } + unlisten = subscribedUnlisten } catch (error) { console.error("[FileTreeTab] failed to listen file watch event:", error) } @@ -2117,6 +2143,7 @@ export function FileTreeTab() { void setup() return () => { + disposed = true if (treeRefreshTimerRef.current) { clearTimeout(treeRefreshTimerRef.current) treeRefreshTimerRef.current = null @@ -2129,8 +2156,7 @@ export function FileTreeTab() { pendingTreeRefreshRef.current = false pendingTreeRefreshNeedsStatusRef.current = false pendingStatusRefreshRef.current = false - unlisten?.() - void stopFileTreeWatch(rootPath) + releaseWatch() } }, [fetchTree, folder?.path, openFilePreview, t]) diff --git a/src/components/layout/aux-panel-git-changes-tab.tsx b/src/components/layout/aux-panel-git-changes-tab.tsx index a87983f..bd82dd2 100644 --- a/src/components/layout/aux-panel-git-changes-tab.tsx +++ b/src/components/layout/aux-panel-git-changes-tab.tsx @@ -615,8 +615,23 @@ export function GitChangesTab() { if (!rootPath || !isChangesTabActive) return let unlisten: (() => void) | null = null + let disposed = false + let watchStarted = false + let watchReleased = false const normalizedRootPath = normalizeComparePath(rootPath) + const releaseWatch = () => { + if (watchReleased) return + watchReleased = true + if (unlisten) { + unlisten() + unlisten = null + } + if (watchStarted) { + void stopFileTreeWatch(rootPath) + } + } + const scheduleRefresh = () => { if (refreshTimerRef.current) { clearTimeout(refreshTimerRef.current) @@ -629,12 +644,17 @@ export function GitChangesTab() { const setup = async () => { try { await startFileTreeWatch(rootPath) + watchStarted = true } catch { // ignore watch startup errors } + if (disposed) { + releaseWatch() + return + } try { - unlisten = await subscribe( + const subscribedUnlisten = await subscribe( "folder://file-tree-changed", (payload) => { if ( @@ -646,6 +666,12 @@ export function GitChangesTab() { scheduleRefresh() } ) + if (disposed) { + subscribedUnlisten() + releaseWatch() + return + } + unlisten = subscribedUnlisten } catch { // ignore listen errors } @@ -654,12 +680,12 @@ export function GitChangesTab() { void setup() return () => { + disposed = true if (refreshTimerRef.current) { clearTimeout(refreshTimerRef.current) refreshTimerRef.current = null } - unlisten?.() - void stopFileTreeWatch(rootPath) + releaseWatch() } }, [fetchChanges, folder?.path, isChangesTabActive])