Skip to content

Commit ceb08b3

Browse files
committed
perf: stale-while-revalidate for issue/PR listing
Serve from cache immediately when available (fresh or stale). Background revalidation for stale entries. Only blocks on synchronous fetch when no cache exists at all. Response time: ~4s → ~20-40ms for cached data.
1 parent 4ec4875 commit ceb08b3

File tree

1 file changed

+38
-10
lines changed

1 file changed

+38
-10
lines changed

packages/server/src/issues/issues.ts

Lines changed: 38 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -40,28 +40,56 @@ export function createIssueTracker(
4040

4141
const tracker: IssueTracker = {
4242
async list(orgId: string, filter?: IssueFilter): Promise<Issue[]> {
43-
// If projectId is specified, get remotes for that project; otherwise we'd need all projects
44-
// For simplicity, if no projectId, we still need remotes — caller should provide via filter
4543
const projectId = filter?.projectId ?? ''
4644
const remotes = getRemotes(orgId, projectId)
4745
const filteredRemotes = filter?.remote ? remotes.filter((r) => r.name === filter.remote) : remotes
4846

4947
let allIssues: Issue[] = []
48+
const staleRemotes: Array<{ remote: (typeof filteredRemotes)[0]; pid: string }> = []
5049

5150
for (const remote of filteredRemotes) {
5251
const pid = projectId || remote.projectId || remote.name
53-
try {
54-
const provider = makeProvider(remote, orgId, pid)
55-
const issues = await provider.list(repoPath(remote), filter)
56-
allIssues.push(...issues)
57-
cache.setCached(orgId, pid, issues)
58-
} catch {
59-
// Provider unreachable — serve from cache
52+
// Serve from cache immediately if fresh enough
53+
if (!cache.isStale(orgId, pid)) {
6054
const cached = cache.getCached(orgId, pid)
61-
if (cached) allIssues.push(...cached)
55+
if (cached) {
56+
allIssues.push(...cached)
57+
continue
58+
}
59+
}
60+
// Cache is stale or missing — try to serve stale data and revalidate in background
61+
const cached = cache.getCached(orgId, pid)
62+
if (cached) {
63+
allIssues.push(...cached)
64+
staleRemotes.push({ remote, pid })
65+
} else {
66+
// No cache at all — must fetch synchronously
67+
try {
68+
const provider = makeProvider(remote, orgId, pid)
69+
const issues = await provider.list(repoPath(remote), filter)
70+
allIssues.push(...issues)
71+
cache.setCached(orgId, pid, issues)
72+
} catch {
73+
// Nothing to serve
74+
}
6275
}
6376
}
6477

78+
// Background revalidation for stale remotes
79+
if (staleRemotes.length > 0) {
80+
void (async () => {
81+
for (const { remote, pid } of staleRemotes) {
82+
try {
83+
const provider = makeProvider(remote, orgId, pid)
84+
const issues = await provider.list(repoPath(remote), filter)
85+
cache.setCached(orgId, pid, issues)
86+
} catch {
87+
// Keep stale cache
88+
}
89+
}
90+
})()
91+
}
92+
6593
// Apply local filters that providers might not support
6694
if (filter?.q) {
6795
const q = filter.q.toLowerCase()

0 commit comments

Comments
 (0)