fix(discord): add TTL and LRU eviction to thread starter cache

Fixes #5260

The DISCORD_THREAD_STARTER_CACHE Map was growing unbounded during
long-running gateway sessions, causing memory exhaustion.

This fix adds:
- 5-minute TTL expiry (thread starters rarely change)
- Max 500 entries with LRU eviction
- Same caching pattern used by Slack's thread resolver

The implementation mirrors src/slack/monitor/thread-resolution.ts
which already handles this correctly.
This commit is contained in:
Web Vijayi
2026-01-31 13:48:50 +05:30
committed by Shadow
parent 149db5b2c2
commit 5882cf2f5d

View File

@@ -29,12 +29,54 @@ type DiscordThreadParentInfo = {
type?: ChannelType;
};
const DISCORD_THREAD_STARTER_CACHE = new Map<string, DiscordThreadStarter>();
// Cache entry with timestamp for TTL-based eviction
type DiscordThreadStarterCacheEntry = {
value: DiscordThreadStarter;
updatedAt: number;
};
// Cache configuration: 5 minute TTL (thread starters rarely change), max 500 entries
const DISCORD_THREAD_STARTER_CACHE_TTL_MS = 5 * 60 * 1000;
const DISCORD_THREAD_STARTER_CACHE_MAX = 500;
const DISCORD_THREAD_STARTER_CACHE = new Map<string, DiscordThreadStarterCacheEntry>();
export function __resetDiscordThreadStarterCacheForTest() {
DISCORD_THREAD_STARTER_CACHE.clear();
}
// Get cached entry with TTL check, refresh LRU position on hit
function getCachedThreadStarter(key: string, now: number): DiscordThreadStarter | undefined {
const entry = DISCORD_THREAD_STARTER_CACHE.get(key);
if (!entry) {
return undefined;
}
// Check TTL expiry
if (now - entry.updatedAt > DISCORD_THREAD_STARTER_CACHE_TTL_MS) {
DISCORD_THREAD_STARTER_CACHE.delete(key);
return undefined;
}
// Refresh LRU position by re-inserting (Map maintains insertion order)
DISCORD_THREAD_STARTER_CACHE.delete(key);
DISCORD_THREAD_STARTER_CACHE.set(key, { ...entry, updatedAt: now });
return entry.value;
}
// Set cached entry with LRU eviction when max size exceeded
function setCachedThreadStarter(key: string, value: DiscordThreadStarter, now: number): void {
// Remove existing entry first (to update LRU position)
DISCORD_THREAD_STARTER_CACHE.delete(key);
DISCORD_THREAD_STARTER_CACHE.set(key, { value, updatedAt: now });
// Evict oldest entries (first in Map) when over max size
while (DISCORD_THREAD_STARTER_CACHE.size > DISCORD_THREAD_STARTER_CACHE_MAX) {
const oldestKey = DISCORD_THREAD_STARTER_CACHE.keys().next().value;
if (!oldestKey) {
break;
}
DISCORD_THREAD_STARTER_CACHE.delete(oldestKey);
}
}
function isDiscordThreadType(type: ChannelType | undefined): boolean {
return (
type === ChannelType.PublicThread ||
@@ -100,7 +142,8 @@ export async function resolveDiscordThreadStarter(params: {
resolveTimestampMs: (value?: string | null) => number | undefined;
}): Promise<DiscordThreadStarter | null> {
const cacheKey = params.channel.id;
const cached = DISCORD_THREAD_STARTER_CACHE.get(cacheKey);
const now = Date.now();
const cached = getCachedThreadStarter(cacheKey, now);
if (cached) {
return cached;
}
@@ -146,7 +189,7 @@ export async function resolveDiscordThreadStarter(params: {
author,
timestamp: timestamp ?? undefined,
};
DISCORD_THREAD_STARTER_CACHE.set(cacheKey, payload);
setCachedThreadStarter(cacheKey, payload, Date.now());
return payload;
} catch {
return null;