From 1022ffb56a2ba6271685609f1f9acfd16645bdf6 Mon Sep 17 00:00:00 2001 From: ranokay Date: Sat, 17 Oct 2026 12:39:09 +0300 Subject: [PATCH] fix(ui): coalesce lyric refreshes with in-flight requests Signed-off-by: ranokay --- ui/src/audioplayer/useEnhancedLyrics.js | 7 +++++- ui/src/audioplayer/useEnhancedLyrics.test.js | 23 ++++++++++++++++++++ ui/src/audioplayer/usePlayerLyrics.test.jsx | 23 +++++++++++--------- 3 files changed, 42 insertions(+), 11 deletions(-) diff --git a/ui/src/audioplayer/useEnhancedLyrics.js b/ui/src/audioplayer/useEnhancedLyrics.js index 2fcc59248..d703e2509 100644 --- a/ui/src/audioplayer/useEnhancedLyrics.js +++ b/ui/src/audioplayer/useEnhancedLyrics.js @@ -153,8 +153,13 @@ const useEnhancedLyrics = ({ const refresh = useCallback(() => { if (!trackId || disabled || !requested) return + // Coalesce with an unsettled request: its answer is at least as fresh as + // one started now, and restarting it would strand slow responses behind + // the poll interval. + const entry = inFlight.get(cacheKey) + if (entry && !entry.settled) return setRefreshToken((current) => current + 1) - }, [disabled, requested, trackId]) + }, [cacheKey, disabled, requested, trackId]) useEffect(() => { if (!trackId || disabled || !requested) { diff --git a/ui/src/audioplayer/useEnhancedLyrics.test.js b/ui/src/audioplayer/useEnhancedLyrics.test.js index 80ca8e8f9..0e1e15b97 100644 --- a/ui/src/audioplayer/useEnhancedLyrics.test.js +++ b/ui/src/audioplayer/useEnhancedLyrics.test.js @@ -344,6 +344,29 @@ describe('useEnhancedLyrics', () => { expect(subsonic.getLyricsBySongId).toHaveBeenCalledTimes(2) }) + it('coalesces a refresh with a request that is still in flight', async () => { + const pending = createDeferred() + subsonic.getLyricsBySongId.mockReturnValueOnce(pending.promise) + + const { result } = renderLyrics({ trackId: 'song-coalesce' }) + + expect(subsonic.getLyricsBySongId).toHaveBeenCalledTimes(1) + + act(() => { + result.current.refresh() + }) + expect(subsonic.getLyricsBySongId).toHaveBeenCalledTimes(1) + + act(() => { + pending.resolve( + responseForEntries([sourceEntry('NetEase', 'NetEase line')]), + ) + }) + + await expectLyric(result, 'NetEase line') + expect(subsonic.getLyricsBySongId).toHaveBeenCalledTimes(1) + }) + it('keeps current lyrics when a refresh fails', async () => { subsonic.getLyricsBySongId .mockResolvedValueOnce( diff --git a/ui/src/audioplayer/usePlayerLyrics.test.jsx b/ui/src/audioplayer/usePlayerLyrics.test.jsx index 8a894f067..4e9d754c5 100644 --- a/ui/src/audioplayer/usePlayerLyrics.test.jsx +++ b/ui/src/audioplayer/usePlayerLyrics.test.jsx @@ -195,29 +195,32 @@ describe('usePlayerLyrics', () => { vi.useFakeTimers() subsonic.getLyricsBySongId.mockResolvedValue(variantsResponse()) + const flushMicrotasks = () => + act(async () => { + await Promise.resolve() + await Promise.resolve() + }) + const { result } = renderPlayerLyrics() act(() => result.current.toolbarLyricsProps.onToggleLyrics()) - await act(async () => { - await Promise.resolve() - }) + await flushMicrotasks() expect(subsonic.getLyricsBySongId).toHaveBeenCalledTimes(1) act(() => result.current.desktopLyricsProps.onSourceMenuOpenChange(true)) expect(subsonic.getLyricsBySongId).toHaveBeenCalledTimes(2) - await act(async () => { - vi.advanceTimersByTime(5_000) - }) - expect(subsonic.getLyricsBySongId).toHaveBeenCalledTimes(3) - - for (let step = 0; step < 11; step += 1) { + let expectedCalls = 2 + for (let step = 0; step < 12; step += 1) { + await flushMicrotasks() await act(async () => { vi.advanceTimersByTime(5_000) }) + expectedCalls += 1 + expect(subsonic.getLyricsBySongId).toHaveBeenCalledTimes(expectedCalls) } - expect(subsonic.getLyricsBySongId).toHaveBeenCalledTimes(14) act(() => result.current.desktopLyricsProps.onSourceMenuOpenChange(false)) + await flushMicrotasks() await act(async () => { vi.advanceTimersByTime(15_000) })