mirror of
https://github.com/openclaw/openclaw.git
synced 2026-06-03 19:34:07 +00:00
fix(msteams): bound parent thread cache expiry
This commit is contained in:
@@ -1,4 +1,4 @@
|
||||
import { beforeEach, describe, expect, it, vi } from "vitest";
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
||||
import type { GraphThreadMessage } from "./graph-thread.js";
|
||||
import {
|
||||
resetThreadParentContextCachesForTest,
|
||||
@@ -95,6 +95,10 @@ describe("fetchParentMessageCached", () => {
|
||||
resetThreadParentContextCachesForTest();
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
vi.useRealTimers();
|
||||
});
|
||||
|
||||
it("invokes the fetcher on first call", async () => {
|
||||
const mockMsg: GraphThreadMessage = {
|
||||
id: "p1",
|
||||
@@ -150,21 +154,31 @@ describe("fetchParentMessageCached", () => {
|
||||
|
||||
it("re-fetches after TTL expires", async () => {
|
||||
vi.useFakeTimers();
|
||||
try {
|
||||
const fetcher = vi.fn(async () => ({
|
||||
id: "p1",
|
||||
body: { content: "hi", contentType: "text" },
|
||||
}));
|
||||
const fetcher = vi.fn(async () => ({
|
||||
id: "p1",
|
||||
body: { content: "hi", contentType: "text" },
|
||||
}));
|
||||
|
||||
await fetchParentMessageCached("tok", "g1", "c1", "p1", fetcher);
|
||||
// 5 min TTL: advance just beyond.
|
||||
vi.advanceTimersByTime(5 * 60 * 1000 + 1);
|
||||
await fetchParentMessageCached("tok", "g1", "c1", "p1", fetcher);
|
||||
await fetchParentMessageCached("tok", "g1", "c1", "p1", fetcher);
|
||||
// 5 min TTL: advance just beyond.
|
||||
vi.advanceTimersByTime(5 * 60 * 1000 + 1);
|
||||
await fetchParentMessageCached("tok", "g1", "c1", "p1", fetcher);
|
||||
|
||||
expect(fetcher).toHaveBeenCalledTimes(2);
|
||||
} finally {
|
||||
vi.useRealTimers();
|
||||
}
|
||||
expect(fetcher).toHaveBeenCalledTimes(2);
|
||||
});
|
||||
|
||||
it("does not cache parent fetches when the expiry would exceed Date range", async () => {
|
||||
vi.useFakeTimers();
|
||||
vi.setSystemTime(new Date(8_640_000_000_000_000));
|
||||
const fetcher = vi.fn(async () => ({
|
||||
id: "p1",
|
||||
body: { content: "hi", contentType: "text" },
|
||||
}));
|
||||
|
||||
await fetchParentMessageCached("tok", "g1", "c1", "p1", fetcher);
|
||||
await fetchParentMessageCached("tok", "g1", "c1", "p1", fetcher);
|
||||
|
||||
expect(fetcher).toHaveBeenCalledTimes(2);
|
||||
});
|
||||
|
||||
it("evicts oldest entries when exceeding the 100-entry cap", async () => {
|
||||
|
||||
@@ -13,6 +13,10 @@
|
||||
// the same parent is not re-injected on every subsequent reply in the
|
||||
// thread.
|
||||
|
||||
import {
|
||||
asDateTimestampMs,
|
||||
resolveExpiresAtMsFromDurationMs,
|
||||
} from "openclaw/plugin-sdk/number-runtime";
|
||||
import { fetchChannelMessage, stripHtmlFromTeamsMessage } from "./graph-thread.js";
|
||||
import type { GraphThreadMessage } from "./graph-thread.js";
|
||||
|
||||
@@ -61,6 +65,13 @@ function buildParentCacheKey(groupId: string, channelId: string, parentId: strin
|
||||
return `${groupId}\u0000${channelId}\u0000${parentId}`;
|
||||
}
|
||||
|
||||
function resolveParentCacheExpiresAt(nowRaw: number): number | undefined {
|
||||
const nowMs = asDateTimestampMs(nowRaw);
|
||||
return nowMs === undefined
|
||||
? undefined
|
||||
: resolveExpiresAtMsFromDurationMs(PARENT_CACHE_TTL_MS, { nowMs });
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetch a channel parent message with an LRU+TTL cache.
|
||||
*
|
||||
@@ -75,16 +86,23 @@ export async function fetchParentMessageCached(
|
||||
fetchParent: ThreadParentContextFetcher = fetchChannelMessage,
|
||||
): Promise<GraphThreadMessage | undefined> {
|
||||
const key = buildParentCacheKey(groupId, channelId, parentId);
|
||||
const now = Date.now();
|
||||
const now = asDateTimestampMs(Date.now());
|
||||
const cached = parentCache.get(key);
|
||||
if (cached && cached.expiresAt > now) {
|
||||
const cachedExpiresAt = cached ? asDateTimestampMs(cached.expiresAt) : undefined;
|
||||
if (cached && now !== undefined && cachedExpiresAt !== undefined && cachedExpiresAt > now) {
|
||||
// Refresh LRU ordering on hit.
|
||||
parentCache.delete(key);
|
||||
parentCache.set(key, cached);
|
||||
return cached.message;
|
||||
}
|
||||
if (cached) {
|
||||
parentCache.delete(key);
|
||||
}
|
||||
const message = await fetchParent(token, groupId, channelId, parentId);
|
||||
touchLru(parentCache, key, { message, expiresAt: now + PARENT_CACHE_TTL_MS }, PARENT_CACHE_MAX);
|
||||
const expiresAt = resolveParentCacheExpiresAt(Date.now());
|
||||
if (expiresAt !== undefined) {
|
||||
touchLru(parentCache, key, { message, expiresAt }, PARENT_CACHE_MAX);
|
||||
}
|
||||
return message;
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user