mirror of
https://github.com/openclaw/openclaw.git
synced 2026-06-12 04:32:53 +00:00
fix(discord): bound channel info cache clocks
This commit is contained in:
@@ -1,3 +1,7 @@
|
||||
import {
|
||||
asDateTimestampMs,
|
||||
resolveExpiresAtMsFromDurationMs,
|
||||
} from "openclaw/plugin-sdk/number-runtime";
|
||||
import { logVerbose } from "openclaw/plugin-sdk/runtime-env";
|
||||
import { normalizeOptionalStringifiedId } from "openclaw/plugin-sdk/string-coerce-runtime";
|
||||
import type { ChannelType, Message } from "../internal/discord.js";
|
||||
@@ -30,6 +34,22 @@ export function resetDiscordChannelInfoCacheForTest() {
|
||||
DISCORD_CHANNEL_INFO_CACHE.clear();
|
||||
}
|
||||
|
||||
function resolveDiscordChannelInfoCacheExpiresAt(ttlMs: number, nowMs: number): number | undefined {
|
||||
return resolveExpiresAtMsFromDurationMs(ttlMs, { nowMs });
|
||||
}
|
||||
|
||||
function cacheDiscordChannelInfo(
|
||||
channelId: string,
|
||||
value: DiscordChannelInfo | null,
|
||||
ttlMs: number,
|
||||
nowMs: number,
|
||||
): void {
|
||||
const expiresAt = resolveDiscordChannelInfoCacheExpiresAt(ttlMs, nowMs);
|
||||
if (expiresAt !== undefined) {
|
||||
DISCORD_CHANNEL_INFO_CACHE.set(channelId, { value, expiresAt });
|
||||
}
|
||||
}
|
||||
|
||||
function normalizeDiscordChannelId(value: unknown): string {
|
||||
return normalizeOptionalStringifiedId(value) ?? "";
|
||||
}
|
||||
@@ -51,9 +71,11 @@ export async function resolveDiscordChannelInfo(
|
||||
client: DiscordChannelInfoClient,
|
||||
channelId: string,
|
||||
): Promise<DiscordChannelInfo | null> {
|
||||
const rawNow = Date.now();
|
||||
const now = asDateTimestampMs(rawNow);
|
||||
const cached = DISCORD_CHANNEL_INFO_CACHE.get(channelId);
|
||||
if (cached) {
|
||||
if (cached.expiresAt > Date.now()) {
|
||||
if (now !== undefined && cached.expiresAt > now) {
|
||||
return cached.value;
|
||||
}
|
||||
DISCORD_CHANNEL_INFO_CACHE.delete(channelId);
|
||||
@@ -61,10 +83,7 @@ export async function resolveDiscordChannelInfo(
|
||||
try {
|
||||
const channel = await client.fetchChannel(channelId);
|
||||
if (!channel) {
|
||||
DISCORD_CHANNEL_INFO_CACHE.set(channelId, {
|
||||
value: null,
|
||||
expiresAt: Date.now() + DISCORD_CHANNEL_INFO_NEGATIVE_CACHE_TTL_MS,
|
||||
});
|
||||
cacheDiscordChannelInfo(channelId, null, DISCORD_CHANNEL_INFO_NEGATIVE_CACHE_TTL_MS, rawNow);
|
||||
return null;
|
||||
}
|
||||
const channelInfo = resolveDiscordChannelInfoSafe(channel);
|
||||
@@ -80,17 +99,11 @@ export async function resolveDiscordChannelInfo(
|
||||
parentId: channelInfo.parentId,
|
||||
ownerId: channelInfo.ownerId,
|
||||
};
|
||||
DISCORD_CHANNEL_INFO_CACHE.set(channelId, {
|
||||
value: payload,
|
||||
expiresAt: Date.now() + DISCORD_CHANNEL_INFO_CACHE_TTL_MS,
|
||||
});
|
||||
cacheDiscordChannelInfo(channelId, payload, DISCORD_CHANNEL_INFO_CACHE_TTL_MS, rawNow);
|
||||
return payload;
|
||||
} catch (err) {
|
||||
logVerbose(`discord: failed to fetch channel ${channelId}: ${String(err)}`);
|
||||
DISCORD_CHANNEL_INFO_CACHE.set(channelId, {
|
||||
value: null,
|
||||
expiresAt: Date.now() + DISCORD_CHANNEL_INFO_NEGATIVE_CACHE_TTL_MS,
|
||||
});
|
||||
cacheDiscordChannelInfo(channelId, null, DISCORD_CHANNEL_INFO_NEGATIVE_CACHE_TTL_MS, rawNow);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,7 +4,7 @@ import {
|
||||
MessageReferenceType,
|
||||
StickerFormatType,
|
||||
} from "discord-api-types/v10";
|
||||
import { beforeAll, beforeEach, describe, expect, it, vi } from "vitest";
|
||||
import { afterEach, beforeAll, beforeEach, describe, expect, it, vi } from "vitest";
|
||||
import { ChannelType, type Client, type Message } from "../internal/discord.js";
|
||||
|
||||
const readRemoteMediaBuffer = vi.fn();
|
||||
@@ -65,6 +65,10 @@ beforeAll(async () => {
|
||||
} = await import("./message-utils.js"));
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
vi.restoreAllMocks();
|
||||
});
|
||||
|
||||
function asMessage(payload: Record<string, unknown>): Message {
|
||||
return payload as unknown as Message;
|
||||
}
|
||||
@@ -1231,4 +1235,37 @@ describe("resolveDiscordChannelInfo", () => {
|
||||
expect(second).toBeNull();
|
||||
expect(fetchChannel).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it("does not reuse cached channel info while the process clock is invalid", async () => {
|
||||
const fetchChannel = vi
|
||||
.fn()
|
||||
.mockResolvedValueOnce({ type: ChannelType.GuildText, name: "old" })
|
||||
.mockResolvedValueOnce({ type: ChannelType.GuildText, name: "fresh" });
|
||||
const client = { fetchChannel } as unknown as Client;
|
||||
|
||||
const first = await resolveDiscordChannelInfo(client, "invalid-clock-channel");
|
||||
expect(first?.name).toBe("old");
|
||||
|
||||
vi.spyOn(Date, "now").mockReturnValue(8_640_000_000_000_001);
|
||||
const second = await resolveDiscordChannelInfo(client, "invalid-clock-channel");
|
||||
|
||||
expect(second?.name).toBe("fresh");
|
||||
expect(fetchChannel).toHaveBeenCalledTimes(2);
|
||||
});
|
||||
|
||||
it("does not cache channel info when the cache expiry would exceed the Date range", async () => {
|
||||
vi.spyOn(Date, "now").mockReturnValue(8_640_000_000_000_000);
|
||||
const fetchChannel = vi
|
||||
.fn()
|
||||
.mockResolvedValueOnce({ type: ChannelType.GuildText, name: "first" })
|
||||
.mockResolvedValueOnce({ type: ChannelType.GuildText, name: "second" });
|
||||
const client = { fetchChannel } as unknown as Client;
|
||||
|
||||
const first = await resolveDiscordChannelInfo(client, "overflow-cache-channel");
|
||||
const second = await resolveDiscordChannelInfo(client, "overflow-cache-channel");
|
||||
|
||||
expect(first?.name).toBe("first");
|
||||
expect(second?.name).toBe("second");
|
||||
expect(fetchChannel).toHaveBeenCalledTimes(2);
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user