-
Notifications
You must be signed in to change notification settings - Fork 545
Expand file tree
/
Copy pathcache.ts
More file actions
116 lines (102 loc) · 3.14 KB
/
cache.ts
File metadata and controls
116 lines (102 loc) · 3.14 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
/**
* Simple in-memory TTL cache for SCM API data.
*
* Reduces GitHub API rate limit exhaustion by caching PR enrichment data.
* Default TTL: 60 seconds (data is fresh enough for dashboard refresh).
*/
interface CacheEntry<T> {
value: T;
expiresAt: number;
}
export const PR_CACHE_TTL_SUCCESS_MS = 5 * 60_000; // 5 minutes
export const PR_CACHE_TTL_RATE_LIMIT_MS = 30_000; // 30 seconds
const DEFAULT_TTL_MS = PR_CACHE_TTL_SUCCESS_MS;
/**
* Simple TTL cache backed by a Map.
* Automatically evicts stale entries on get() and periodically cleans up.
*/
export class TTLCache<T> {
private cache = new Map<string, CacheEntry<T>>();
private readonly ttlMs: number;
private cleanupInterval?: ReturnType<typeof setInterval>;
constructor(ttlMs: number = DEFAULT_TTL_MS) {
this.ttlMs = ttlMs;
// Run cleanup every TTL period to prevent memory leaks from unread keys
this.cleanupInterval = setInterval(() => this.evictExpired(), ttlMs);
// Ensure cleanup interval doesn't prevent Node process from exiting
if (this.cleanupInterval.unref) {
this.cleanupInterval.unref();
}
}
/** Get a cached value if it exists and isn't stale */
get(key: string): T | null {
const entry = this.cache.get(key);
if (!entry) return null;
if (Date.now() > entry.expiresAt) {
this.cache.delete(key);
return null;
}
return entry.value;
}
/** Set a cache entry with TTL (optional override) */
set(key: string, value: T, ttlMs?: number): void {
this.cache.set(key, {
value,
expiresAt: Date.now() + (ttlMs ?? this.ttlMs),
});
}
/** Evict all expired entries */
private evictExpired(): void {
const now = Date.now();
for (const [key, entry] of this.cache.entries()) {
if (now > entry.expiresAt) {
this.cache.delete(key);
}
}
}
/** Clear all entries and stop cleanup interval */
clear(): void {
this.cache.clear();
if (this.cleanupInterval) {
clearInterval(this.cleanupInterval);
this.cleanupInterval = undefined;
}
}
/** Get cache size (includes stale entries) */
size(): number {
return this.cache.size;
}
}
/**
* Enrichment data for a single PR.
* Cached by PR number (key: `owner/repo#123`).
*/
export interface PREnrichmentData {
state: "open" | "merged" | "closed";
title: string;
additions: number;
deletions: number;
ciStatus: "none" | "pending" | "passing" | "failing";
ciChecks: Array<{ name: string; status: "pending" | "running" | "passed" | "failed" | "skipped"; url?: string }>;
reviewDecision: "none" | "pending" | "approved" | "changes_requested";
mergeability: {
mergeable: boolean;
ciPassing: boolean;
approved: boolean;
noConflicts: boolean;
blockers: string[];
};
unresolvedThreads: number;
unresolvedComments: Array<{
url: string;
path: string;
author: string;
body: string;
}>;
}
/** Global PR enrichment cache (60s TTL) */
export const prCache = new TTLCache<PREnrichmentData>();
/** Generate cache key for a PR: `owner/repo#123` */
export function prCacheKey(owner: string, repo: string, number: number): string {
return `${owner}/${repo}#${number}`;
}