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, + });