Pre-launch — Gluecron is in final validation. Public signups and git hosting for non-owner users open after launch review.
CodeIssuesDiscussionsWikiPull RequestsProjectsCommitsActionsReleasesContributorsPulse● GatesSecuritySettingsDeploymentsPipelineInsightsAgents✨ Explain✨ Ask AI✨ Workspace✨ Spec✨ Tests▓ Debt Map✨ NL Search🏛 Archaeology
Blame · Line-by-line history

cache.ts

Each line is annotated with the commit that last touched it. Click any SHA to jump to that commit and see the surrounding change.

cache.tsBlame123 lines · 1 contributor
05b973eClaude1/**
2 * In-memory LRU cache with TTL expiration.
3 *
4 * Used for caching git operations, session lookups,
5 * and other hot-path data to avoid redundant subprocess
6 * spawns and database roundtrips.
7 */
8
9interface CacheEntry<T> {
10 value: T;
11 expiresAt: number;
12}
13
14export class LRUCache<T> {
15 private cache = new Map<string, CacheEntry<T>>();
16 private readonly maxSize: number;
17 private readonly ttlMs: number;
18
19 constructor(maxSize: number, ttlMs: number) {
20 this.maxSize = maxSize;
21 this.ttlMs = ttlMs;
22 }
23
24 get(key: string): T | undefined {
25 const entry = this.cache.get(key);
26 if (!entry) return undefined;
27
28 if (Date.now() > entry.expiresAt) {
29 this.cache.delete(key);
30 return undefined;
31 }
32
33 // Move to end (most recently used)
34 this.cache.delete(key);
35 this.cache.set(key, entry);
36 return entry.value;
37 }
38
39 set(key: string, value: T): void {
40 // Delete first to update position
41 this.cache.delete(key);
42
43 // Evict oldest if at capacity
44 if (this.cache.size >= this.maxSize) {
45 const firstKey = this.cache.keys().next().value;
46 if (firstKey !== undefined) this.cache.delete(firstKey);
47 }
48
49 this.cache.set(key, {
50 value,
51 expiresAt: Date.now() + this.ttlMs,
52 });
53 }
54
55 invalidate(key: string): void {
56 this.cache.delete(key);
57 }
58
59 /**
60 * Invalidate all keys matching a prefix.
61 * Useful for clearing all cached data for a repo after a push.
62 */
63 invalidatePrefix(prefix: string): void {
64 for (const key of this.cache.keys()) {
65 if (key.startsWith(prefix)) {
66 this.cache.delete(key);
67 }
68 }
69 }
70
71 clear(): void {
72 this.cache.clear();
73 }
74
75 get size(): number {
76 return this.cache.size;
77 }
78}
79
80// --- Shared cache instances ---
81
82/** Git operation cache — trees, branches, commits, blobs (5 min TTL, 2000 entries) */
83export const gitCache = new LRUCache<unknown>(2000, 5 * 60 * 1000);
84
85/** Session cache — maps session tokens to user objects (2 min TTL, 500 entries) */
86export const sessionCache = new LRUCache<unknown>(500, 2 * 60 * 1000);
87
88/**
89 * Cache-through helper — returns cached value or runs the factory,
90 * caches the result, and returns it.
91 *
92 * Does NOT cache empty arrays or null — avoids stale empty results
93 * when a repo is freshly created or still receiving its first push.
94 */
95export async function cached<T>(
96 cache: LRUCache<T>,
97 key: string,
98 factory: () => Promise<T>
99): Promise<T> {
100 const existing = cache.get(key);
101 if (existing !== undefined) return existing;
102
103 const value = await factory();
104
105 // Only cache non-empty results
106 if (value !== null && value !== undefined) {
107 if (Array.isArray(value) && value.length === 0) {
108 // Don't cache empty arrays — repo may just be initializing
109 } else {
110 cache.set(key, value);
111 }
112 }
113
114 return value;
115}
116
117/**
118 * Invalidate all cached data for a repository.
119 * Call this after pushes, merges, or any repo-mutating operation.
120 */
121export function invalidateRepoCache(owner: string, repo: string): void {
122 gitCache.invalidatePrefix(`${owner}/${repo}:`);
123}