Coalesce stale conflict polling (#2081)

This commit is contained in:
Neil 2026-05-16 10:56:18 -07:00 committed by GitHub
parent b4a5c142a7
commit 53f03255dd
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
3 changed files with 110 additions and 3 deletions

View File

@ -0,0 +1,58 @@
import { describe, expect, it, vi } from 'vitest'
import { createCoalescedPollRunner } from './coalesced-poll-runner'
function deferred(): {
promise: Promise<void>
resolve: () => void
} {
let resolve: () => void = () => {}
const promise = new Promise<void>((r) => {
resolve = r
})
return { promise, resolve }
}
async function flushMicrotasks(): Promise<void> {
await Promise.resolve()
await Promise.resolve()
}
describe('createCoalescedPollRunner', () => {
it('keeps one task in flight and runs one trailing task after skipped triggers', async () => {
const calls: ReturnType<typeof deferred>[] = []
const task = vi.fn(() => {
const call = deferred()
calls.push(call)
return call.promise
})
const runner = createCoalescedPollRunner(task)
runner.run()
runner.run()
runner.run()
await flushMicrotasks()
expect(task).toHaveBeenCalledTimes(1)
calls[0]?.resolve()
await flushMicrotasks()
expect(task).toHaveBeenCalledTimes(2)
calls[1]?.resolve()
await flushMicrotasks()
expect(task).toHaveBeenCalledTimes(2)
})
it('drops queued trailing work after disposal', async () => {
const call = deferred()
const task = vi.fn(() => call.promise)
const runner = createCoalescedPollRunner(task)
runner.run()
runner.run()
runner.dispose()
call.resolve()
await flushMicrotasks()
expect(task).toHaveBeenCalledTimes(1)
})
})

View File

@ -0,0 +1,43 @@
export type CoalescedPollRunner = {
run: () => void
dispose: () => void
}
export function createCoalescedPollRunner(task: () => Promise<void>): CoalescedPollRunner {
let disposed = false
let inFlight = false
let rerun = false
const run = (): void => {
if (disposed) {
return
}
if (inFlight) {
rerun = true
return
}
inFlight = true
void task()
.catch(() => {
// Poll callers handle their own expected transient errors. A rejected
// task must still release the in-flight latch and optional trailing run.
})
.finally(() => {
inFlight = false
if (rerun && !disposed) {
rerun = false
run()
return
}
rerun = false
})
}
return {
run,
dispose: () => {
disposed = true
rerun = false
}
}
}

View File

@ -6,6 +6,7 @@ import { isGitRepoKind } from '../../../../shared/repo-kind'
import { getConnectionId } from '@/lib/connection-context'
import { getRuntimeGitConflictOperation } from '@/runtime/runtime-git-client'
import { refreshGitStatusForWorktree } from './git-status-refresh'
import { createCoalescedPollRunner } from './coalesced-poll-runner'
const POLL_INTERVAL_MS = 3000
@ -164,15 +165,20 @@ export function useGitStatusPolling(): void {
}
}
void pollStale()
// Why: remote conflict probes can exceed the 3s interval. Keep one poll in
// flight and coalesce skipped ticks into one trailing pass so stale badges
// catch up without stacking SSH/RPC work.
const pollRunner = createCoalescedPollRunner(pollStale)
pollRunner.run()
const intervalId = setInterval(() => {
if (document.hasFocus()) {
void pollStale()
pollRunner.run()
}
}, POLL_INTERVAL_MS)
const onFocus = (): void => void pollStale()
const onFocus = (): void => pollRunner.run()
window.addEventListener('focus', onFocus)
return () => {
pollRunner.dispose()
clearInterval(intervalId)
window.removeEventListener('focus', onFocus)
}