Merge branch 'main' into main

This commit is contained in:
github-actions[bot]
2026-04-29 04:50:54 +00:00
committed by GitHub
2 changed files with 49 additions and 3 deletions

View File

@@ -138,10 +138,51 @@ describe('useVaultWatcher', () => {
unmount()
await settleWatcherSubscription()
expect(mocks.unlisten).toHaveBeenCalledOnce()
expect(mocks.invoke).toHaveBeenCalledWith('stop_vault_watcher')
})
it('swallows stale native watcher unlisten failures and still stops the watcher', async () => {
mocks.unlisten.mockImplementationOnce(() => {
throw new TypeError("undefined is not an object (evaluating 'listeners[eventId].handlerId')")
})
const { unmount } = renderHook(() => useVaultWatcher({
vaultPath: '/vault',
onVaultChanged: vi.fn(),
}))
await settleWatcherSubscription()
expect(() => unmount()).not.toThrow()
await settleWatcherSubscription()
expect(mocks.unlisten).toHaveBeenCalledOnce()
expect(mocks.invoke).toHaveBeenCalledWith('stop_vault_watcher')
})
it('keeps listener replacement stable when vault paths churn', async () => {
mocks.unlisten.mockImplementationOnce(() => {
throw new TypeError("undefined is not an object (evaluating 'listeners[eventId].handlerId')")
})
const { rerender } = renderHook(
({ vaultPath }) => useVaultWatcher({ vaultPath, onVaultChanged: vi.fn() }),
{ initialProps: { vaultPath: '/vault-a' } },
)
await settleWatcherSubscription()
expect(() => rerender({ vaultPath: '/vault-b' })).not.toThrow()
await settleWatcherSubscription()
expect(mocks.listen).toHaveBeenCalledTimes(2)
expect(mocks.unlisten).toHaveBeenCalledOnce()
expect(mocks.invoke).toHaveBeenCalledWith('start_vault_watcher', { path: '/vault-a' })
expect(mocks.invoke).toHaveBeenCalledWith('start_vault_watcher', { path: '/vault-b' })
expect(mocks.invoke).toHaveBeenCalledWith('stop_vault_watcher')
})
it('batches changed paths for the active vault', async () => {
const onVaultChanged = vi.fn()
renderHook(() => useVaultWatcher({ vaultPath: '/vault', onVaultChanged }))

View File

@@ -200,6 +200,12 @@ function handleWatcherEvent({
}
}
function cleanupNativeWatcherListener(unlisten: UnlistenFn): void {
void Promise.resolve()
.then(unlisten)
.catch(() => {})
}
function usePendingVaultRefresh({
vaultPathRef,
onVaultChanged,
@@ -263,7 +269,6 @@ function useNativeVaultWatcher({
enqueueChangedPaths: (paths: WatchPath[]) => void
clearPendingRefresh: () => void
}) {
useEffect(() => {
const root = normalizeWatchPath(vaultPath)
if (!root || !isTauri()) return
@@ -275,7 +280,7 @@ function useNativeVaultWatcher({
handleWatcherEvent({ event, root, enqueueChangedPaths })
}).then((nextUnlisten) => {
if (cancelled) {
nextUnlisten()
cleanupNativeWatcherListener(nextUnlisten)
} else {
unlisten = nextUnlisten
}
@@ -290,7 +295,7 @@ function useNativeVaultWatcher({
return () => {
cancelled = true
clearPendingRefresh()
unlisten?.()
if (unlisten) cleanupNativeWatcherListener(unlisten)
void invoke('stop_vault_watcher').catch(() => {})
}
}, [vaultPath, enqueueChangedPaths, clearPendingRefresh])