Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 4 additions & 10 deletions src-gui/src/store/middleware/storeListener.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
import { createListenerMiddleware } from "@reduxjs/toolkit";
import { throttle, debounce } from "lodash";
import {
getAllSwapInfos,
getAllSwapTimelocks,
Expand Down Expand Up @@ -32,31 +31,26 @@ 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<typeof throttle>
ReturnType<typeof createSwapInfoUpdater>
>();

// 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}`);
});
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);
Expand Down
58 changes: 58 additions & 0 deletions src-gui/src/store/middleware/swapInfoUpdater.test.ts
Original file line number Diff line number Diff line change
@@ -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();
});
});
9 changes: 9 additions & 0 deletions src-gui/src/store/middleware/swapInfoUpdater.ts
Original file line number Diff line number Diff line change
@@ -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,
});