diff --git a/public/index.html b/public/index.html
index 00ac223..5fb6f3e 100644
--- a/public/index.html
+++ b/public/index.html
@@ -125,13 +125,13 @@
el.channelSelect.value = state.selectedVoiceChannel;
el.channelFilter.value = state.selectedTextChannel;
applyActiveTab(state.activeTab);
+ if ((textChanged || textGuildChanged) && state.selectedTextChannel && state.selectedTextGuild) {
+ await apiRequest('/api/backlog-sync', {
+ method: 'POST',
+ body: JSON.stringify({ guildId: state.selectedTextGuild, channelId: state.selectedTextChannel }),
+ }).catch((error) => showError(`Backlog sync failed: ${error.message}`));
+ }
if (textChanged || textGuildChanged || state.activeTab === 'text') {
- if (state.selectedTextChannel && state.selectedTextGuild) {
- await apiRequest('/api/backlog-sync', {
- method: 'POST',
- body: JSON.stringify({ guildId: state.selectedTextGuild, channelId: state.selectedTextChannel }),
- }).catch((error) => showError(`Backlog sync failed: ${error.message}`));
- }
await fetchText().catch((error) => showError(error.message));
}
await reconcileListenState();
diff --git a/src/routes/syncRoutes.ts b/src/routes/syncRoutes.ts
index 981df0b..1a80597 100644
--- a/src/routes/syncRoutes.ts
+++ b/src/routes/syncRoutes.ts
@@ -6,6 +6,24 @@ import { createChildLogger } from "../logger";
import { syncSelectedChannelBacklog } from "../moderation/backlogSync";
const logger = createChildLogger("sync-routes");
+const BACKLOG_SYNC_COOLDOWN_MS = 5 * 60 * 1000;
+const recentBacklogSyncs = new Map();
+
+export function shouldSkipRecentBacklogSync(
+ guildId: string,
+ channelId: string,
+ now = Date.now(),
+): boolean {
+ const key = `${guildId}:${channelId}`;
+ const lastSync = recentBacklogSyncs.get(key);
+ if (lastSync && now - lastSync < BACKLOG_SYNC_COOLDOWN_MS) return true;
+ recentBacklogSyncs.set(key, now);
+ return false;
+}
+
+export function clearRecentBacklogSyncs(): void {
+ recentBacklogSyncs.clear();
+}
export function createSyncRoutes(client: Client): Router {
const router = express.Router();
@@ -26,6 +44,17 @@ export function createSyncRoutes(client: Client): Router {
);
}
+ if (shouldSkipRecentBacklogSync(guildId, channelId)) {
+ logger.debug({ guildId, channelId }, "Skipping recent backlog sync");
+ res.json({
+ success: true,
+ channelId,
+ messagesSync: 0,
+ skipped: true,
+ });
+ return;
+ }
+
logger.info({ guildId, channelId }, "Starting backlog sync");
const count = await syncSelectedChannelBacklog(
@@ -43,6 +72,7 @@ export function createSyncRoutes(client: Client): Router {
success: true,
channelId,
messagesSync: count,
+ skipped: false,
});
} catch (error) {
next(error);
diff --git a/tests/routes/syncRoutes.test.ts b/tests/routes/syncRoutes.test.ts
index 5664017..2dc9eda 100644
--- a/tests/routes/syncRoutes.test.ts
+++ b/tests/routes/syncRoutes.test.ts
@@ -1,6 +1,10 @@
import type { Request, Response } from "express";
import { beforeEach, describe, expect, it, vi } from "vitest";
-import { createSyncRoutes } from "../../src/routes/syncRoutes";
+import {
+ clearRecentBacklogSyncs,
+ createSyncRoutes,
+ shouldSkipRecentBacklogSync,
+} from "../../src/routes/syncRoutes";
const syncSelectedChannelBacklog = vi.hoisted(() => vi.fn());
@@ -11,6 +15,7 @@ vi.mock("../../src/moderation/backlogSync", () => ({
describe("createSyncRoutes", () => {
beforeEach(() => {
syncSelectedChannelBacklog.mockReset();
+ clearRecentBacklogSyncs();
});
it("syncs the selected guild and channel from the request", async () => {
@@ -41,7 +46,52 @@ describe("createSyncRoutes", () => {
success: true,
channelId: "selected-channel",
messagesSync: 3,
+ skipped: false,
});
expect(next).not.toHaveBeenCalled();
});
+
+ it("skips repeated sync requests during the cooldown window", async () => {
+ expect(shouldSkipRecentBacklogSync("guild", "channel", 1000)).toBe(false);
+ expect(shouldSkipRecentBacklogSync("guild", "channel", 1001)).toBe(true);
+ });
+
+ it("allows repeated sync requests after the cooldown window", async () => {
+ expect(shouldSkipRecentBacklogSync("guild", "channel", 1000)).toBe(false);
+ expect(shouldSkipRecentBacklogSync("guild", "channel", 301001)).toBe(false);
+ });
+
+ it("does not call Discord backlog sync for repeated requests", async () => {
+ syncSelectedChannelBacklog.mockResolvedValue(3);
+ const router = createSyncRoutes({} as never);
+ const route = router.stack.find(
+ (layer) => layer.route?.path === "/backlog-sync",
+ );
+ const handler = route?.route?.stack[0]?.handle;
+
+ await handler?.(
+ {
+ body: { guildId: "selected-guild", channelId: "selected-channel" },
+ } as Request,
+ { json: vi.fn() } as unknown as Response,
+ vi.fn(),
+ );
+
+ const json = vi.fn();
+ await handler?.(
+ {
+ body: { guildId: "selected-guild", channelId: "selected-channel" },
+ } as Request,
+ { json } as unknown as Response,
+ vi.fn(),
+ );
+
+ expect(syncSelectedChannelBacklog).toHaveBeenCalledTimes(1);
+ expect(json).toHaveBeenCalledWith({
+ success: true,
+ channelId: "selected-channel",
+ messagesSync: 0,
+ skipped: true,
+ });
+ });
});