From d5ef1a6338d06fb960fbd143966290e6b38cd3d4 Mon Sep 17 00:00:00 2001 From: lh-0610 <215683234+lh-0610@users.noreply.github.com> Date: Sat, 1 Aug 2026 12:17:43 +0900 Subject: [PATCH] Fix delayed swap info refresh on progress events The swap info refresher wrapped a 3s debounce inside a 2s throttle. The throttle released a call every 2s and each release reset the debounce timer. Because 2s < 3s the timer never expired while progress events kept arriving, so the GUI stopped refreshing for as long as the swap kept making progress. A single isolated event still waited 3s, and after the last event of a burst the refresh took up to 5s: up to 2s for the throttle trailing edge plus the 3s debounce window. Drop the inner debounce and keep only the 2s throttle with its existing leading and trailing edges. The first event now refreshes immediately, rapid events coalesce, a continuous stream refreshes every 2s, and the final event is not lost. The throttle layer is unchanged, so no refresh can land later than it did before. The throttle stays to rate limit the backend: each refresh issues both getSwapInfo and getSwapTimelock. Extract the factory into swapInfoUpdater.ts. storeListener.ts pulls in the whole Tauri RPC and Redux graph, which cannot be imported into a unit test without heavy mocking. Add three tests: the first event refreshes immediately, rapid events collapse into one leading plus one trailing refresh, and a continuous event stream keeps refreshing instead of stalling. The last one is the regression test for this bug. Closes #822 --- src-gui/src/store/middleware/storeListener.ts | 14 ++--- .../store/middleware/swapInfoUpdater.test.ts | 58 +++++++++++++++++++ .../src/store/middleware/swapInfoUpdater.ts | 9 +++ 3 files changed, 71 insertions(+), 10 deletions(-) create mode 100644 src-gui/src/store/middleware/swapInfoUpdater.test.ts create mode 100644 src-gui/src/store/middleware/swapInfoUpdater.ts diff --git a/src-gui/src/store/middleware/storeListener.ts b/src-gui/src/store/middleware/storeListener.ts index 38fb9dfefe..3438f2c784 100644 --- a/src-gui/src/store/middleware/storeListener.ts +++ b/src-gui/src/store/middleware/storeListener.ts @@ -1,5 +1,4 @@ import { createListenerMiddleware } from "@reduxjs/toolkit"; -import { throttle, debounce } from "lodash"; import { getAllSwapInfos, getAllSwapTimelocks, @@ -32,19 +31,19 @@ import { setConversation, } from "store/features/conversationsSlice"; import { setBitcoinAddress } from "store/features/bitcoinWalletSlice"; +import { createSwapInfoUpdater } from "store/middleware/swapInfoUpdater"; // Create a Map to store throttled functions per swap_id const throttledGetSwapInfoFunctions = new Map< string, - ReturnType + ReturnType >(); // Function to get or create a throttled getSwapInfo for a specific swap_id const getThrottledSwapInfoUpdater = (swapId: string) => { if (!throttledGetSwapInfoFunctions.has(swapId)) { - // Create a throttled function that executes at most once every 2 seconds - // but will wait for 3 seconds of quiet during rapid calls (using debounce) - const debouncedGetSwapInfo = debounce(() => { + // Refresh immediately, then coalesce rapid follow-up progress events. + const throttledFunction = createSwapInfoUpdater(() => { logger.debug(`Executing getSwapInfo for swap ${swapId}`); getSwapInfo(swapId).catch((error) => { logger.debug(`Failed to fetch swap info for swap ${swapId}: ${error}`); @@ -52,11 +51,6 @@ const getThrottledSwapInfoUpdater = (swapId: string) => { getSwapTimelock(swapId).catch((error) => { logger.debug(`Failed to fetch timelock for swap ${swapId}: ${error}`); }); - }, 3000); // 3 seconds debounce for rapid calls - - const throttledFunction = throttle(debouncedGetSwapInfo, 2000, { - leading: true, // Execute immediately on first call - trailing: true, // Execute on trailing edge if needed }); throttledGetSwapInfoFunctions.set(swapId, throttledFunction); diff --git a/src-gui/src/store/middleware/swapInfoUpdater.test.ts b/src-gui/src/store/middleware/swapInfoUpdater.test.ts new file mode 100644 index 0000000000..81430fd045 --- /dev/null +++ b/src-gui/src/store/middleware/swapInfoUpdater.test.ts @@ -0,0 +1,58 @@ +import { afterEach, describe, expect, it, vi } from "vitest"; +import { + createSwapInfoUpdater, + SWAP_INFO_UPDATE_INTERVAL_MS, +} from "./swapInfoUpdater"; + +afterEach(() => { + vi.useRealTimers(); +}); + +describe("createSwapInfoUpdater", () => { + it("updates immediately on the first progress event", () => { + vi.useFakeTimers(); + const update = vi.fn(); + const updater = createSwapInfoUpdater(update); + + updater(); + + expect(update).toHaveBeenCalledOnce(); + updater.cancel(); + }); + + it("coalesces rapid progress events into one trailing update", () => { + vi.useFakeTimers(); + const update = vi.fn(); + const updater = createSwapInfoUpdater(update); + + updater(); + updater(); + updater(); + + expect(update).toHaveBeenCalledOnce(); + vi.advanceTimersByTime(SWAP_INFO_UPDATE_INTERVAL_MS - 1); + expect(update).toHaveBeenCalledOnce(); + vi.advanceTimersByTime(1); + expect(update).toHaveBeenCalledTimes(2); + updater.cancel(); + }); + + it("keeps updating during a continuous stream of progress events", () => { + vi.useFakeTimers(); + const update = vi.fn(); + const updater = createSwapInfoUpdater(update); + + updater(); + for ( + let elapsed = 0; + elapsed < SWAP_INFO_UPDATE_INTERVAL_MS * 2; + elapsed += 500 + ) { + vi.advanceTimersByTime(500); + updater(); + } + + expect(update).toHaveBeenCalledTimes(3); + updater.cancel(); + }); +}); diff --git a/src-gui/src/store/middleware/swapInfoUpdater.ts b/src-gui/src/store/middleware/swapInfoUpdater.ts new file mode 100644 index 0000000000..7467599976 --- /dev/null +++ b/src-gui/src/store/middleware/swapInfoUpdater.ts @@ -0,0 +1,9 @@ +import { throttle } from "lodash"; + +export const SWAP_INFO_UPDATE_INTERVAL_MS = 2_000; + +export const createSwapInfoUpdater = (update: () => void) => + throttle(update, SWAP_INFO_UPDATE_INTERVAL_MS, { + leading: true, + trailing: true, + });