Prefer exact PR lookups and stale sidebar refresh

Prefer exact linked PR lookup when safe, add stale-while-revalidate for sidebar hosted review metadata, and preserve branch discovery correctness for stale linked PR metadata.
This commit is contained in:
Neil
2026-05-15 22:42:17 -07:00
committed by GitHub
parent 5b88551e3b
commit 4bfd1eb157
6 changed files with 584 additions and 99 deletions
@@ -656,7 +656,8 @@ function SourceControlInner(): React.JSX.Element {
void fetchHostedReviewForBranch(activeRepo.path, branchName, {
repoId: activeRepo.id,
linkedGitHubPR,
linkedGitLabMR
linkedGitLabMR,
staleWhileRevalidate: true
})
}, [
activeRepo,
@@ -211,7 +211,8 @@ const WorktreeCard = React.memo(function WorktreeCard({
fetchHostedReviewForBranch(repo.path, branch, {
repoId: repo.id,
linkedGitHubPR: worktree.linkedPR ?? null,
linkedGitLabMR: worktree.linkedGitLabMR ?? null
linkedGitLabMR: worktree.linkedGitLabMR ?? null,
staleWhileRevalidate: true
})
}
}, [
@@ -1,7 +1,11 @@
import { beforeEach, describe, expect, it, vi } from 'vitest'
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
import { create } from 'zustand'
import type { AppState } from '../types'
import { createHostedReviewSlice, refreshHostedReviewCard } from './hosted-review'
import {
createHostedReviewSlice,
getHostedReviewCacheKey,
refreshHostedReviewCard
} from './hosted-review'
import type { HostedReviewInfo } from '../../../../shared/hosted-review'
const runtimeRpc = vi.hoisted(() => ({
@@ -52,6 +56,10 @@ describe('hosted review slice', () => {
runtimeRpc.callRuntimeRpc.mockReset()
})
afterEach(() => {
vi.useRealTimers()
})
it('fetches and caches branch review status through the common IPC surface', async () => {
mockApi.hostedReview.forBranch.mockResolvedValueOnce(review)
const store = makeStore()
@@ -201,4 +209,79 @@ describe('hosted review slice', () => {
await expect(firstLinkedFetch).resolves.toEqual(review)
await expect(secondLinkedFetch).resolves.toEqual(review)
})
it('serves stale hosted review metadata while revalidating in the background', async () => {
vi.useFakeTimers()
vi.setSystemTime(0)
const updatedReview: HostedReviewInfo = {
...review,
title: 'Updated linked PR status',
status: 'failure',
updatedAt: '2026-05-10T00:01:01.000Z'
}
let resolveRefresh: (value: typeof updatedReview) => void = () => {}
const refresh = new Promise<typeof updatedReview>((resolve) => {
resolveRefresh = resolve
})
mockApi.hostedReview.forBranch
.mockResolvedValueOnce(review)
.mockReturnValueOnce(refresh as Promise<HostedReviewInfo>)
const store = makeStore()
await expect(
store.getState().fetchHostedReviewForBranch('/repo', 'feature/pr', {
linkedGitHubPR: 42
})
).resolves.toEqual(review)
vi.setSystemTime(60_001)
await expect(
store.getState().fetchHostedReviewForBranch('/repo', 'feature/pr', {
linkedGitHubPR: 42,
staleWhileRevalidate: true
})
).resolves.toEqual(review)
await expect(
store.getState().fetchHostedReviewForBranch('/repo', 'feature/pr', {
linkedGitHubPR: 42,
staleWhileRevalidate: true
})
).resolves.toEqual(review)
expect(mockApi.hostedReview.forBranch).toHaveBeenCalledTimes(2)
const cacheKey = getHostedReviewCacheKey('/repo', 'feature/pr')
expect(store.getState().hostedReviewCache[cacheKey]?.data).toEqual(review)
resolveRefresh(updatedReview)
await refresh
await Promise.resolve()
expect(store.getState().hostedReviewCache[cacheKey]?.data).toEqual(updatedReview)
})
it('does not serve stale metadata when a stronger linked PR hint changes the lookup', async () => {
vi.useFakeTimers()
vi.setSystemTime(0)
const linkedReview: HostedReviewInfo = {
...review,
provider: 'github',
number: 42,
title: 'Exact linked PR',
url: 'https://github.com/acme/orca/pull/42'
}
mockApi.hostedReview.forBranch.mockResolvedValueOnce(review).mockResolvedValueOnce(linkedReview)
const store = makeStore()
await expect(store.getState().fetchHostedReviewForBranch('/repo', 'feature/pr')).resolves.toBe(
review
)
vi.setSystemTime(60_001)
await expect(
store.getState().fetchHostedReviewForBranch('/repo', 'feature/pr', {
linkedGitHubPR: 42,
staleWhileRevalidate: true
})
).resolves.toEqual(linkedReview)
expect(mockApi.hostedReview.forBranch).toHaveBeenCalledTimes(2)
})
})
+84 -66
View File
@@ -11,7 +11,7 @@ import { callRuntimeRpc, getActiveRuntimeTarget } from '@/runtime/runtime-rpc-cl
import type { AppState } from '../types'
type CacheEntry<T> = { data: T | null; fetchedAt: number; linkedReviewHintKey?: string }
type FetchOptions = { force?: boolean; repoId?: string }
type FetchOptions = { force?: boolean; repoId?: string; staleWhileRevalidate?: boolean }
type LinkedReviewHints = {
linkedGitHubPR?: number | null
linkedGitLabMR?: number | null
@@ -36,7 +36,7 @@ function isFresh<T>(entry: CacheEntry<T> | undefined): entry is CacheEntry<T> {
return entry !== undefined && Date.now() - entry.fetchedAt < CACHE_TTL_MS
}
// Why: a branch-only null is weaker than a null after trying the persisted
// Why: a branch-keyed lookup can describe a different PR than the persisted
// linked review number. Track that distinction without changing the cache key.
function linkedReviewHintKey(options?: LinkedReviewHints): string {
const hints = [
@@ -51,11 +51,11 @@ function linkedReviewHintKey(options?: LinkedReviewHints): string {
.join('|')
}
function shouldRefetchNullForLinkedHint(
function shouldRefetchForLinkedHint(
cached: CacheEntry<HostedReviewInfo> | undefined,
hintKey: string
): boolean {
return cached?.data === null && hintKey !== '' && (cached.linkedReviewHintKey ?? '') !== hintKey
return cached !== undefined && hintKey !== '' && (cached.linkedReviewHintKey ?? '') !== hintKey
}
function canReuseInflightHint(inflightHintKey: string, nextHintKey: string): boolean {
@@ -175,7 +175,7 @@ export const createHostedReviewSlice: StateCreator<AppState, [], [], HostedRevie
const cacheKey = getHostedReviewCacheKey(repoPath, branch, settings, options?.repoId)
const cached = get().hostedReviewCache[cacheKey]
const hintKey = linkedReviewHintKey(options)
const linkedRefetch = shouldRefetchNullForLinkedHint(cached, hintKey)
const linkedRefetch = shouldRefetchForLinkedHint(cached, hintKey)
if (!options?.force && !linkedRefetch && isFresh(cached)) {
return cached.data
}
@@ -184,70 +184,88 @@ export const createHostedReviewSlice: StateCreator<AppState, [], [], HostedRevie
const inflightHasRequestedHint =
inflightRequest !== undefined &&
canReuseInflightHint(inflightRequest.linkedReviewHintKey, hintKey)
const startRequest = (): Promise<HostedReviewInfo | null> => {
const generation = (requestGenerations.get(cacheKey) ?? 0) + 1
requestGenerations.set(cacheKey, generation)
const request = (async () => {
try {
const args = {
branch,
...(options?.repoId !== undefined ? { repoId: options.repoId } : {}),
linkedGitHubPR: options?.linkedGitHubPR ?? null,
linkedGitLabMR: options?.linkedGitLabMR ?? null,
linkedBitbucketPR: options?.linkedBitbucketPR ?? null,
linkedGiteaPR: options?.linkedGiteaPR ?? null
}
const review =
target.kind === 'environment'
? await callRuntimeRpc<HostedReviewInfo | null>(
target,
'hostedReview.forBranch',
{ repo: options?.repoId ?? repoPath, repoPath, ...args },
// Why: remote dev boxes can be slower at `git`/`gh` lookups
// than local desktop repos, especially on Windows filesystem
// paths. The main-process queue caps concurrency, so a longer
// timeout no longer risks a background socket stampede.
{ timeoutMs: 30_000 }
)
: await window.api.hostedReview.forBranch({ repoPath, ...args })
if (requestGenerations.get(cacheKey) === generation) {
set((state) => ({
hostedReviewCache: {
...state.hostedReviewCache,
[cacheKey]: { data: review, fetchedAt: Date.now(), linkedReviewHintKey: hintKey }
}
}))
}
return review
} catch (error) {
console.error('Failed to fetch hosted review:', error)
if (requestGenerations.get(cacheKey) === generation) {
set((state) => ({
hostedReviewCache: {
...state.hostedReviewCache,
[cacheKey]: { data: null, fetchedAt: Date.now(), linkedReviewHintKey: hintKey }
}
}))
}
return null
} finally {
const activeRequest = inflightHostedReviewRequests.get(cacheKey)
if (activeRequest?.generation === generation) {
inflightHostedReviewRequests.delete(cacheKey)
}
}
})()
inflightHostedReviewRequests.set(cacheKey, {
promise: request,
force: Boolean(options?.force),
generation,
linkedReviewHintKey: hintKey
})
return request
}
if (
!options?.force &&
!linkedRefetch &&
options?.staleWhileRevalidate &&
cached !== undefined &&
cached.data !== null
) {
// Why: sidebar PR metadata can stay visible while a quiet refresh updates
// it; don't block card rendering on a quota-bound GitHub round trip.
if (!inflightRequest || !inflightHasRequestedHint) {
void startRequest()
}
return cached.data
}
if (inflightRequest && (!options?.force || inflightRequest.force) && inflightHasRequestedHint) {
return inflightRequest.promise
}
const generation = (requestGenerations.get(cacheKey) ?? 0) + 1
requestGenerations.set(cacheKey, generation)
const request = (async () => {
try {
const args = {
branch,
...(options?.repoId !== undefined ? { repoId: options.repoId } : {}),
linkedGitHubPR: options?.linkedGitHubPR ?? null,
linkedGitLabMR: options?.linkedGitLabMR ?? null,
linkedBitbucketPR: options?.linkedBitbucketPR ?? null,
linkedGiteaPR: options?.linkedGiteaPR ?? null
}
const review =
target.kind === 'environment'
? await callRuntimeRpc<HostedReviewInfo | null>(
target,
'hostedReview.forBranch',
{ repo: options?.repoId ?? repoPath, repoPath, ...args },
// Why: remote dev boxes can be slower at `git`/`gh` lookups
// than local desktop repos, especially on Windows filesystem
// paths. The main-process queue caps concurrency, so a longer
// timeout no longer risks a background socket stampede.
{ timeoutMs: 30_000 }
)
: await window.api.hostedReview.forBranch({ repoPath, ...args })
if (requestGenerations.get(cacheKey) === generation) {
set((state) => ({
hostedReviewCache: {
...state.hostedReviewCache,
[cacheKey]: { data: review, fetchedAt: Date.now(), linkedReviewHintKey: hintKey }
}
}))
}
return review
} catch (error) {
console.error('Failed to fetch hosted review:', error)
if (requestGenerations.get(cacheKey) === generation) {
set((state) => ({
hostedReviewCache: {
...state.hostedReviewCache,
[cacheKey]: { data: null, fetchedAt: Date.now(), linkedReviewHintKey: hintKey }
}
}))
}
return null
} finally {
const activeRequest = inflightHostedReviewRequests.get(cacheKey)
if (activeRequest?.generation === generation) {
inflightHostedReviewRequests.delete(cacheKey)
}
}
})()
inflightHostedReviewRequests.set(cacheKey, {
promise: request,
force: Boolean(options?.force),
generation,
linkedReviewHintKey: hintKey
})
return request
return startRequest()
}
})