feat: implement backlog sync cooldown mechanism and update related tests
This commit is contained in:
@@ -125,13 +125,13 @@
|
|||||||
el.channelSelect.value = state.selectedVoiceChannel;
|
el.channelSelect.value = state.selectedVoiceChannel;
|
||||||
el.channelFilter.value = state.selectedTextChannel;
|
el.channelFilter.value = state.selectedTextChannel;
|
||||||
applyActiveTab(state.activeTab);
|
applyActiveTab(state.activeTab);
|
||||||
if (textChanged || textGuildChanged || state.activeTab === 'text') {
|
if ((textChanged || textGuildChanged) && state.selectedTextChannel && state.selectedTextGuild) {
|
||||||
if (state.selectedTextChannel && state.selectedTextGuild) {
|
|
||||||
await apiRequest('/api/backlog-sync', {
|
await apiRequest('/api/backlog-sync', {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
body: JSON.stringify({ guildId: state.selectedTextGuild, channelId: state.selectedTextChannel }),
|
body: JSON.stringify({ guildId: state.selectedTextGuild, channelId: state.selectedTextChannel }),
|
||||||
}).catch((error) => showError(`Backlog sync failed: ${error.message}`));
|
}).catch((error) => showError(`Backlog sync failed: ${error.message}`));
|
||||||
}
|
}
|
||||||
|
if (textChanged || textGuildChanged || state.activeTab === 'text') {
|
||||||
await fetchText().catch((error) => showError(error.message));
|
await fetchText().catch((error) => showError(error.message));
|
||||||
}
|
}
|
||||||
await reconcileListenState();
|
await reconcileListenState();
|
||||||
|
|||||||
@@ -6,6 +6,24 @@ import { createChildLogger } from "../logger";
|
|||||||
import { syncSelectedChannelBacklog } from "../moderation/backlogSync";
|
import { syncSelectedChannelBacklog } from "../moderation/backlogSync";
|
||||||
|
|
||||||
const logger = createChildLogger("sync-routes");
|
const logger = createChildLogger("sync-routes");
|
||||||
|
const BACKLOG_SYNC_COOLDOWN_MS = 5 * 60 * 1000;
|
||||||
|
const recentBacklogSyncs = new Map<string, number>();
|
||||||
|
|
||||||
|
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 {
|
export function createSyncRoutes(client: Client): Router {
|
||||||
const router = express.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");
|
logger.info({ guildId, channelId }, "Starting backlog sync");
|
||||||
|
|
||||||
const count = await syncSelectedChannelBacklog(
|
const count = await syncSelectedChannelBacklog(
|
||||||
@@ -43,6 +72,7 @@ export function createSyncRoutes(client: Client): Router {
|
|||||||
success: true,
|
success: true,
|
||||||
channelId,
|
channelId,
|
||||||
messagesSync: count,
|
messagesSync: count,
|
||||||
|
skipped: false,
|
||||||
});
|
});
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
next(error);
|
next(error);
|
||||||
|
|||||||
@@ -1,6 +1,10 @@
|
|||||||
import type { Request, Response } from "express";
|
import type { Request, Response } from "express";
|
||||||
import { beforeEach, describe, expect, it, vi } from "vitest";
|
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());
|
const syncSelectedChannelBacklog = vi.hoisted(() => vi.fn());
|
||||||
|
|
||||||
@@ -11,6 +15,7 @@ vi.mock("../../src/moderation/backlogSync", () => ({
|
|||||||
describe("createSyncRoutes", () => {
|
describe("createSyncRoutes", () => {
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
syncSelectedChannelBacklog.mockReset();
|
syncSelectedChannelBacklog.mockReset();
|
||||||
|
clearRecentBacklogSyncs();
|
||||||
});
|
});
|
||||||
|
|
||||||
it("syncs the selected guild and channel from the request", async () => {
|
it("syncs the selected guild and channel from the request", async () => {
|
||||||
@@ -41,7 +46,52 @@ describe("createSyncRoutes", () => {
|
|||||||
success: true,
|
success: true,
|
||||||
channelId: "selected-channel",
|
channelId: "selected-channel",
|
||||||
messagesSync: 3,
|
messagesSync: 3,
|
||||||
|
skipped: false,
|
||||||
});
|
});
|
||||||
expect(next).not.toHaveBeenCalled();
|
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,
|
||||||
|
});
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
Reference in New Issue
Block a user