fix(discord): bound REST entity cache clocks

This commit is contained in:
Peter Steinberger
2026-05-30 10:38:19 -04:00
parent c39fbdb698
commit 9ef5a9afdc
2 changed files with 55 additions and 2 deletions

View File

@@ -342,6 +342,47 @@ describe("Client.deployCommands", () => {
await client.fetchChannel("c1");
expect(get).toHaveBeenCalledTimes(2);
});
it("does not reuse cached REST objects while the process clock is invalid", async () => {
const client = createInternalTestClient();
const get = vi
.fn()
.mockResolvedValueOnce({ id: "c1", type: 0, name: "old" })
.mockResolvedValueOnce({ id: "c1", type: 0, name: "fresh" })
.mockResolvedValueOnce({ id: "c1", type: 0, name: "recovered" });
attachRestMock(client, { get });
const first = await client.fetchChannel("c1");
expect(first.name).toBe("old");
vi.spyOn(Date, "now").mockReturnValue(8_640_000_000_000_001);
const second = await client.fetchChannel("c1");
expect(second.name).toBe("fresh");
vi.mocked(Date.now).mockReturnValue(1_000);
const third = await client.fetchChannel("c1");
expect(third.name).toBe("recovered");
expect(get).toHaveBeenCalledTimes(3);
});
it("does not cache REST objects when the cache expiry would exceed the Date range", async () => {
const client = createInternalTestClient();
const get = vi
.fn()
.mockResolvedValueOnce({ id: "c1", type: 0, name: "first" })
.mockResolvedValueOnce({ id: "c1", type: 0, name: "second" });
attachRestMock(client, { get });
vi.spyOn(Date, "now").mockReturnValue(8_640_000_000_000_000);
const first = await client.fetchChannel("c1");
const second = await client.fetchChannel("c1");
expect(first.name).toBe("first");
expect(second.name).toBe("second");
expect(get).toHaveBeenCalledTimes(2);
});
});
describe("Client gateway event queue", () => {

View File

@@ -1,4 +1,8 @@
import { GatewayDispatchEvents } from "discord-api-types/v10";
import {
asDateTimestampMs,
resolveExpiresAtMsFromDurationMs,
} from "openclaw/plugin-sdk/number-runtime";
import { getChannel, getGuild, getGuildMember, getUser } from "./api.js";
import type { RequestClient } from "./rest.js";
import { Guild, GuildMember, User, channelFactory, type StructureClient } from "./structures.js";
@@ -79,15 +83,23 @@ export class DiscordEntityCache {
private async fetchCached<T>(key: string, fetcher: () => Promise<T>): Promise<T> {
const ttl = this.params.ttlMs ?? DEFAULT_REST_CACHE_TTL_MS;
const rawNow = Date.now();
const now = asDateTimestampMs(rawNow);
if (ttl > 0) {
const cached = this.entries.get(key) as CacheEntry<T> | undefined;
if (cached && cached.expiresAt > Date.now()) {
if (cached && now !== undefined && cached.expiresAt > now) {
return cached.value;
}
if (cached) {
this.entries.delete(key);
}
}
const value = await fetcher();
if (ttl > 0) {
this.entries.set(key, { expiresAt: Date.now() + ttl, value });
const expiresAt = resolveExpiresAtMsFromDurationMs(ttl, { nowMs: rawNow });
if (expiresAt !== undefined) {
this.entries.set(key, { expiresAt, value });
}
}
return value;
}