Files
curriculum-project-hub/hub/src/feishu/senderCache.ts
T
hongjr03 4479f88f4f feat: add sender profile cache with LRU eviction
- SenderNameCache: TTL 10min, LRU max 2048 entries, getOrFetch pattern
- resolveSenderName: API call with cache integration
- Trigger uses cache for approval card resolutions and audit metadata
- Add unit tests (9) for cache lifecycle, LRU, expiry, and null handling
2026-07-08 17:22:23 +08:00

83 lines
1.9 KiB
TypeScript

export interface SenderCacheOptions {
readonly ttlMs?: number;
readonly maxSize?: number;
}
interface SenderCacheEntry {
readonly name: string;
readonly expiresAt: number;
}
const DEFAULT_TTL_MS = 600_000;
const DEFAULT_MAX_SIZE = 2_048;
export class SenderNameCache {
private readonly ttlMs: number;
private readonly maxSize: number;
private readonly entries = new Map<string, SenderCacheEntry>();
constructor(options: SenderCacheOptions = {}) {
this.ttlMs = options.ttlMs ?? DEFAULT_TTL_MS;
this.maxSize = Math.max(0, options.maxSize ?? DEFAULT_MAX_SIZE);
}
get(openId: string): string | null {
const entry = this.entries.get(openId);
if (entry === undefined) return null;
if (entry.expiresAt <= Date.now()) {
this.entries.delete(openId);
return null;
}
this.entries.delete(openId);
this.entries.set(openId, entry);
return entry.name;
}
set(openId: string, name: string): void {
if (this.maxSize === 0) return;
this.entries.delete(openId);
this.entries.set(openId, { name, expiresAt: Date.now() + this.ttlMs });
while (this.entries.size > this.maxSize) {
const oldest = this.entries.keys().next().value;
if (oldest === undefined) break;
this.entries.delete(oldest);
}
}
async getOrFetch(
openId: string,
fetchFn: (openId: string) => Promise<string | null>,
): Promise<string | null> {
const cached = this.get(openId);
if (cached !== null) return cached;
const fetched = await fetchFn(openId);
if (fetched !== null) {
this.set(openId, fetched);
}
return fetched;
}
clear(): void {
this.entries.clear();
}
size(): number {
this.pruneExpired();
return this.entries.size;
}
private pruneExpired(): void {
const now = Date.now();
for (const [openId, entry] of this.entries) {
if (entry.expiresAt <= now) {
this.entries.delete(openId);
}
}
}
}