fix(desktop): coalesce pending live status refreshes

This commit is contained in:
Xipong
2026-09-09 01:16:35 +03:00
committed by Teknium
parent 3bfd7b653b
commit 3d4c537584
2 changed files with 58 additions and 6 deletions
@@ -9,7 +9,12 @@ import { useBackgroundSync } from './use-background-sync'
const noop = () => undefined
const requestGateway = async () => ({ sessions: [] })
function render(activeGatewayProfile: string, activeConnectionId: string, refreshSessions: () => Promise<void>) {
function render(
activeGatewayProfile: string,
activeConnectionId: string,
refreshSessions: () => Promise<void>,
gatewayRequest = requestGateway
) {
return renderHook(
({ connectionId, profile }: { connectionId: string; profile: string }) => {
useBackgroundSync({
@@ -26,7 +31,7 @@ function render(activeGatewayProfile: string, activeConnectionId: string, refres
refreshHermesConfig: noop,
refreshMessagingSessions: noop,
refreshSessions,
requestGateway
requestGateway: gatewayRequest
})
},
{ initialProps: { connectionId: activeConnectionId, profile: activeGatewayProfile } }
@@ -47,6 +52,39 @@ describe('useBackgroundSync profile-scoped session refresh', () => {
vi.useRealTimers()
})
it('coalesces change ticks while the live status request is pending', async () => {
$changeEventsAvailable.set(true)
let release!: (value: { sessions: [] }) => void
const pending = new Promise<{ sessions: [] }>(resolve => { release = resolve })
const request = vi.fn(() => pending)
render('default', 'local', async () => undefined, request)
await act(async () => undefined)
for (let tick = 1; tick <= 8; tick += 1) {
await act(async () => { $sessionsChangeTick.set(tick) })
}
expect(request).toHaveBeenCalledTimes(1)
await act(async () => { release({ sessions: [] }) })
expect(request).toHaveBeenCalledTimes(2)
})
it('discards a queued old-connection refresh when the connection changes', async () => {
let rejectOld!: (reason: Error) => void
const oldRequest = new Promise<{ sessions: [] }>((_, reject) => { rejectOld = reject })
const request = vi.fn().mockReturnValueOnce(oldRequest).mockResolvedValue({ sessions: [] })
const hook = render('default', 'first', async () => undefined, request)
await act(async () => { $sessionsChangeTick.set(1) })
hook.rerender({ connectionId: 'second', profile: 'default' })
await act(async () => undefined)
expect(request).toHaveBeenCalledTimes(2)
await act(async () => { rejectOld(new Error('old connection closed')) })
expect(request).toHaveBeenCalledTimes(2)
hook.unmount()
await act(async () => { $sessionsChangeTick.set(2) })
expect(request).toHaveBeenCalledTimes(2)
})
it('refreshes the session list after the active gateway profile changes', async () => {
const refreshSessions = vi.fn(async () => undefined)
const hook = render('default', 'local', refreshSessions)
@@ -575,7 +575,6 @@ export function useBackgroundSync({
}: BackgroundSyncParams): void {
const changeEventsAvailable = useStore($changeEventsAvailable)
const cronChangeTick = useStore($cronChangeTick)
const sessionsChangeTick = useStore($sessionsChangeTick)
const activeTranscriptBusy = useStore($busy)
const activeTranscriptRefreshPendingRef = useRef<string | null>(null)
// Tile reconcile state (#93942 slice 1): shared sequence guard + per-tile
@@ -682,9 +681,16 @@ export function useBackgroundSync({
let cancelled = false
let inFlight = false
let refreshPending = false
const refreshLiveStatuses = async () => {
if (cancelled) {
return
}
if (inFlight) {
refreshPending = true
return
}
@@ -701,9 +707,16 @@ export function useBackgroundSync({
// still work as before; leave the current sidebar state untouched.
} finally {
inFlight = false
if (refreshPending && !cancelled) {
refreshPending = false
void refreshLiveStatuses()
}
}
}
const unsubscribe = $sessionsChangeTick.listen(() => void refreshLiveStatuses())
const dispose = visiblePoll(
changeEventsAvailable ? LIVE_SESSION_STATUS_BACKSTOP_INTERVAL_MS : LIVE_SESSION_STATUS_POLL_INTERVAL_MS,
() => void refreshLiveStatuses()
@@ -713,11 +726,12 @@ export function useBackgroundSync({
return () => {
cancelled = true
unsubscribe()
dispose()
}
// sessionsChangeTick: each sessions.changed broadcast re-seeds immediately
// via the effect re-run (already coalesced to 2s server-side).
}, [activeGatewayProfile, changeEventsAvailable, gatewayState, requestGateway, sessionsChangeTick])
// Keep the in-flight guard alive across change ticks; a slow response must
// not create a new request (and invalidate the old result) on every tick.
}, [activeConnectionId, activeGatewayProfile, changeEventsAvailable, gatewayState, requestGateway])
// sessions.changed also means the *stored* list may have new rows (a cron
// run's session, an inbound messaging turn creating a thread). The full list