swadhinbiswas feat: push-mirror, webhook queue, auth-cache, gists/discussions · 8/28/2026
96005fc
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
117
118
119
120
121
122
123
124
125
126
127
128
129
130
/**
* Short-TTL in-memory cache for middleware auth lookups.
*
* The request pipeline resolves the full user row (and session row) on every
* request — 2 DB queries per request under load. This cache absorbs that cost
* with a small, bounded, short-TTL window. Revocation latency is therefore
* bounded by AUTH_CACHE_TTL_MS; logout calls invalidateAuthCache for
* immediate effect.
*
* Hit/miss outcomes are recorded via the cacheHits/cacheMisses Prometheus
* counters from src/lib/metrics.ts.
*/
import { cacheHits, cacheMisses } from "@/lib/metrics";
import { logger } from "@/lib/logger";
const TTL_MS = Math.max(
1000,
parseInt(process.env.AUTH_CACHE_TTL_MS || "15000", 10),
);
const MAX_ENTRIES = 5000;
interface CacheEntry<T> {
value: T;
expiresAt: number;
}
const userCache = new Map<string, CacheEntry<unknown>>();
interface SessionCacheEntry extends CacheEntry<unknown> {
ownerUserId?: string;
}
const sessionCache = new Map<string, SessionCacheEntry>();
function evictOldest(
map: Map<string, CacheEntry<unknown> | SessionCacheEntry>,
): void {
while (map.size >= MAX_ENTRIES) {
const oldest = map.keys().next().value;
if (oldest === undefined) break;
map.delete(oldest);
}
}
function getCached(
map: Map<string, CacheEntry<unknown>>,
key: string,
): unknown | undefined {
const entry = map.get(key);
if (!entry) {
cacheMisses.inc();
return undefined;
}
if (Date.now() > entry.expiresAt) {
map.delete(key);
cacheMisses.inc();
return undefined;
}
cacheHits.inc();
return entry.value;
}
function setCached(
map: Map<string, CacheEntry<unknown> | SessionCacheEntry>,
key: string,
value: unknown,
ownerUserId?: string,
): void {
evictOldest(map);
map.set(key, {
value,
expiresAt: Date.now() + TTL_MS,
...(ownerUserId ? { ownerUserId } : {}),
});
}
export async function cachedUserLookup<T>(
userId: string,
lookup: () => Promise<T | null>,
): Promise<T | null> {
const hit = getCached(userCache, userId);
if (hit !== undefined) return hit as T;
const value = await lookup();
if (value !== null && value !== undefined) {
setCached(userCache, userId, value);
}
return value ?? null;
}
export async function cachedSessionLookup<T>(
sessionId: string,
lookup: () => Promise<T | null>,
): Promise<T | null> {
const hit = getCached(sessionCache, sessionId);
if (hit !== undefined) return hit as T;
const value = await lookup();
if (
value !== null &&
value !== undefined &&
typeof value === "object" &&
"userId" in (value as Record<string, unknown>)
) {
setCached(
sessionCache,
sessionId,
value,
(value as { userId?: string }).userId,
);
}
return value ?? null;
}
/** Invalidate all cached entries for a user (call on logout/password change). */
export function invalidateAuthCache(userId?: string, sessionId?: string): void {
if (userId) userCache.delete(userId);
if (sessionId) sessionCache.delete(sessionId);
logger.debug({ userId, sessionId }, "Auth cache invalidated");
}
/**
* Drop every cached session row owned by a user — used when all their
* sessions are revoked server-side (password change/reset) so cached rows
* cannot outlive the revocation for the TTL window.
*/
export function revokeUserSessionCache(userId: string): void {
userCache.delete(userId);
for (const [sessionId, entry] of sessionCache) {
if (entry.ownerUserId === userId) sessionCache.delete(sessionId);
}
logger.debug({ userId }, "User session cache revoked");
}